diff --git a/Moose Mission Setup/Moose.lua b/Moose Mission Setup/Moose.lua index 3f4dc04c5..d7f21b843 100644 --- a/Moose Mission Setup/Moose.lua +++ b/Moose Mission Setup/Moose.lua @@ -1,58602 +1,90 @@ -env.info( '*** MOOSE STATIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20171006_1228' ) +env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) +env.info( 'Moose Generation Timestamp: 20171006_1300' ) ---- Various routines --- @module routines --- @author Flightcontrol +local base = _G -env.setErrorMessageBoxEnabled(false) +__Moose = {} ---- Extract of MIST functions. --- @author Grimes - -routines = {} - - --- don't change these -routines.majorVersion = 3 -routines.minorVersion = 3 -routines.build = 22 - ------------------------------------------------------------------------------------------------------------------ - ----------------------------------------------------------------------------------------------- --- Utils- conversion, Lua utils, etc. -routines.utils = {} - ---from http://lua-users.org/wiki/CopyTable -routines.utils.deepCopy = function(object) - local lookup_table = {} - local function _copy(object) - if type(object) ~= "table" then - return object - elseif lookup_table[object] then - return lookup_table[object] - end - local new_table = {} - lookup_table[object] = new_table - for index, value in pairs(object) do - new_table[_copy(index)] = _copy(value) - end - return setmetatable(new_table, getmetatable(object)) - end - local objectreturn = _copy(object) - return objectreturn -end - - --- porting in Slmod's serialize_slmod2 -routines.utils.oneLineSerialize = function(tbl) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function - - lookup_table = {} - - local function _Serialize( tbl ) - - if type(tbl) == 'table' then --function only works for tables! - - if lookup_table[tbl] then - return lookup_table[object] - end - - local tbl_str = {} - - lookup_table[tbl] = tbl_str - - tbl_str[#tbl_str + 1] = '{' - - for ind,val in pairs(tbl) do -- serialize its fields - local ind_str = {} - if type(ind) == "number" then - ind_str[#ind_str + 1] = '[' - ind_str[#ind_str + 1] = tostring(ind) - ind_str[#ind_str + 1] = ']=' - else --must be a string - ind_str[#ind_str + 1] = '[' - ind_str[#ind_str + 1] = routines.utils.basicSerialize(ind) - ind_str[#ind_str + 1] = ']=' - end - - local val_str = {} - if ((type(val) == 'number') or (type(val) == 'boolean')) then - val_str[#val_str + 1] = tostring(val) - val_str[#val_str + 1] = ',' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'string' then - val_str[#val_str + 1] = routines.utils.basicSerialize(val) - val_str[#val_str + 1] = ',' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'nil' then -- won't ever happen, right? - val_str[#val_str + 1] = 'nil,' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'table' then - if ind == "__index" then - -- tbl_str[#tbl_str + 1] = "__index" - -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it - else - - val_str[#val_str + 1] = _Serialize(val) - val_str[#val_str + 1] = ',' --I think this is right, I just added it - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - end - elseif type(val) == 'function' then - -- tbl_str[#tbl_str + 1] = "function " .. tostring(ind) - -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it - else --- env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)) --- env.info( debug.traceback() ) - end - - end - tbl_str[#tbl_str + 1] = '}' - return table.concat(tbl_str) +__Moose.Include = function( IncludeFile ) + if not __Moose.Includes[ IncludeFile ] then + __Moose.Includes[IncludeFile] = IncludeFile + local f = assert( base.loadfile( __Moose.ProgramPath .. IncludeFile ) ) + if f == nil then + error ("Moose: Could not load Moose file " .. IncludeFile ) else - return tostring(tbl) - end - end - - local objectreturn = _Serialize(tbl) - return objectreturn -end - ---porting in Slmod's "safestring" basic serialize -routines.utils.basicSerialize = function(s) - if s == nil then - return "\"\"" - else - if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'table') or (type(s) == 'userdata') ) then - return tostring(s) - elseif type(s) == 'string' then - s = string.format('%q', s) - return s - end - end -end - - -routines.utils.toDegree = function(angle) - return angle*180/math.pi -end - -routines.utils.toRadian = function(angle) - return angle*math.pi/180 -end - -routines.utils.metersToNM = function(meters) - return meters/1852 -end - -routines.utils.metersToFeet = function(meters) - return meters/0.3048 -end - -routines.utils.NMToMeters = function(NM) - return NM*1852 -end - -routines.utils.feetToMeters = function(feet) - return feet*0.3048 -end - -routines.utils.mpsToKnots = function(mps) - return mps*3600/1852 -end - -routines.utils.mpsToKmph = function(mps) - return mps*3.6 -end - -routines.utils.knotsToMps = function(knots) - return knots*1852/3600 -end - -routines.utils.kmphToMps = function(kmph) - return kmph/3.6 -end - -function routines.utils.makeVec2(Vec3) - if Vec3.z then - return {x = Vec3.x, y = Vec3.z} - else - return {x = Vec3.x, y = Vec3.y} -- it was actually already vec2. - end -end - -function routines.utils.makeVec3(Vec2, y) - if not Vec2.z then - if not y then - y = 0 - end - return {x = Vec2.x, y = y, z = Vec2.y} - else - return {x = Vec2.x, y = Vec2.y, z = Vec2.z} -- it was already Vec3, actually. - end -end - -function routines.utils.makeVec3GL(Vec2, offset) - local adj = offset or 0 - - if not Vec2.z then - return {x = Vec2.x, y = (land.getHeight(Vec2) + adj), z = Vec2.y} - else - return {x = Vec2.x, y = (land.getHeight({x = Vec2.x, y = Vec2.z}) + adj), z = Vec2.z} - end -end - -routines.utils.zoneToVec3 = function(zone) - local new = {} - if type(zone) == 'table' and zone.point then - new.x = zone.point.x - new.y = zone.point.y - new.z = zone.point.z - return new - elseif type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - if zone then - new.x = zone.point.x - new.y = zone.point.y - new.z = zone.point.z - return new - end - end -end - --- gets heading-error corrected direction from point along vector vec. -function routines.utils.getDir(vec, point) - local dir = math.atan2(vec.z, vec.x) - dir = dir + routines.getNorthCorrection(point) - if dir < 0 then - dir = dir + 2*math.pi -- put dir in range of 0 to 2*pi - end - return dir -end - --- gets distance in meters between two points (2 dimensional) -function routines.utils.get2DDist(point1, point2) - point1 = routines.utils.makeVec3(point1) - point2 = routines.utils.makeVec3(point2) - return routines.vec.mag({x = point1.x - point2.x, y = 0, z = point1.z - point2.z}) -end - --- gets distance in meters between two points (3 dimensional) -function routines.utils.get3DDist(point1, point2) - return routines.vec.mag({x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z}) -end - - - - - ---3D Vector manipulation -routines.vec = {} - -routines.vec.add = function(vec1, vec2) - return {x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z} -end - -routines.vec.sub = function(vec1, vec2) - return {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z} -end - -routines.vec.scalarMult = function(vec, mult) - return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult} -end - -routines.vec.scalar_mult = routines.vec.scalarMult - -routines.vec.dp = function(vec1, vec2) - return vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z -end - -routines.vec.cp = function(vec1, vec2) - return { x = vec1.y*vec2.z - vec1.z*vec2.y, y = vec1.z*vec2.x - vec1.x*vec2.z, z = vec1.x*vec2.y - vec1.y*vec2.x} -end - -routines.vec.mag = function(vec) - return (vec.x^2 + vec.y^2 + vec.z^2)^0.5 -end - -routines.vec.getUnitVec = function(vec) - local mag = routines.vec.mag(vec) - return { x = vec.x/mag, y = vec.y/mag, z = vec.z/mag } -end - -routines.vec.rotateVec2 = function(vec2, theta) - return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)} -end ---------------------------------------------------------------------------------------------------------------------------- - - - - --- acc- the accuracy of each easting/northing. 0, 1, 2, 3, 4, or 5. -routines.tostringMGRS = function(MGRS, acc) - if acc == 0 then - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph - else - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Easting/(10^(5-acc)), 0)) - .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Northing/(10^(5-acc)), 0)) - end -end - ---[[acc: -in DM: decimal point of minutes. -In DMS: decimal point of seconds. -position after the decimal of the least significant digit: -So: -42.32 - acc of 2. -]] -routines.tostringLL = function(lat, lon, acc, DMS) - - local latHemi, lonHemi - if lat > 0 then - latHemi = 'N' - else - latHemi = 'S' - end - - if lon > 0 then - lonHemi = 'E' - else - lonHemi = 'W' - end - - lat = math.abs(lat) - lon = math.abs(lon) - - local latDeg = math.floor(lat) - local latMin = (lat - latDeg)*60 - - local lonDeg = math.floor(lon) - local lonMin = (lon - lonDeg)*60 - - if DMS then -- degrees, minutes, and seconds. - local oldLatMin = latMin - latMin = math.floor(latMin) - local latSec = routines.utils.round((oldLatMin - latMin)*60, acc) - - local oldLonMin = lonMin - lonMin = math.floor(lonMin) - local lonSec = routines.utils.round((oldLonMin - lonMin)*60, acc) - - if latSec == 60 then - latSec = 0 - latMin = latMin + 1 - end - - if lonSec == 60 then - lonSec = 0 - lonMin = lonMin + 1 - end - - local secFrmtStr -- create the formatting string for the seconds place - if acc <= 0 then -- no decimal place. - secFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end - - return string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi - - else -- degrees, decimal minutes. - latMin = routines.utils.round(latMin, acc) - lonMin = routines.utils.round(lonMin, acc) - - if latMin == 60 then - latMin = 0 - latDeg = latDeg + 1 - end - - if lonMin == 60 then - lonMin = 0 - lonDeg = lonDeg + 1 - end - - local minFrmtStr -- create the formatting string for the minutes place - if acc <= 0 then -- no decimal place. - minFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end - - return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi - - end -end - ---[[ required: az - radian - required: dist - meters - optional: alt - meters (set to false or nil if you don't want to use it). - optional: metric - set true to get dist and alt in km and m. - precision will always be nearest degree and NM or km.]] -routines.tostringBR = function(az, dist, alt, metric) - az = routines.utils.round(routines.utils.toDegree(az), 0) - - if metric then - dist = routines.utils.round(dist/1000, 2) - else - dist = routines.utils.round(routines.utils.metersToNM(dist), 2) - end - - local s = string.format('%03d', az) .. ' for ' .. dist - - if alt then - if metric then - s = s .. ' at ' .. routines.utils.round(alt, 0) - else - s = s .. ' at ' .. routines.utils.round(routines.utils.metersToFeet(alt), 0) - end - end - return s -end - -routines.getNorthCorrection = function(point) --gets the correction needed for true north - if not point.z then --Vec2; convert to Vec3 - point.z = point.y - point.y = 0 - end - local lat, lon = coord.LOtoLL(point) - local north_posit = coord.LLtoLO(lat + 1, lon) - return math.atan2(north_posit.z - point.z, north_posit.x - point.x) -end - - -do - local idNum = 0 - - --Simplified event handler - routines.addEventHandler = function(f) --id is optional! - local handler = {} - idNum = idNum + 1 - handler.id = idNum - handler.f = f - handler.onEvent = function(self, event) - self.f(event) - end - world.addEventHandler(handler) - end - - routines.removeEventHandler = function(id) - for key, handler in pairs(world.eventHandlers) do - if handler.id and handler.id == id then - world.eventHandlers[key] = nil - return true - end - end - return false - end -end - --- need to return a Vec3 or Vec2? -function routines.getRandPointInCircle(point, radius, innerRadius) - local theta = 2*math.pi*math.random() - local rad = math.random() + math.random() - if rad > 1 then - rad = 2 - rad - end - - local radMult - if innerRadius and innerRadius <= radius then - radMult = (radius - innerRadius)*rad + innerRadius - else - radMult = radius*rad - end - - if not point.z then --might as well work with vec2/3 - point.z = point.y - end - - local rndCoord - if radius > 0 then - rndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z} - else - rndCoord = {x = point.x, y = point.z} - end - return rndCoord -end - -routines.goRoute = function(group, path) - local misTask = { - id = 'Mission', - params = { - route = { - points = routines.utils.deepCopy(path), - }, - }, - } - if type(group) == 'string' then - group = Group.getByName(group) - end - local groupCon = group:getController() - if groupCon then - groupCon:setTask(misTask) - return true - end - - Controller.setTask(groupCon, misTask) - return false -end - - --- Useful atomic functions from mist, ported. - -routines.ground = {} -routines.fixedWing = {} -routines.heli = {} - -routines.ground.buildWP = function(point, overRideForm, overRideSpeed) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - local form, speed - - if point.speed and not overRideSpeed then - wp.speed = point.speed - elseif type(overRideSpeed) == 'number' then - wp.speed = overRideSpeed - else - wp.speed = routines.utils.kmphToMps(20) - end - - if point.form and not overRideForm then - form = point.form - else - form = overRideForm - end - - if not form then - wp.action = 'Cone' - else - form = string.lower(form) - if form == 'off_road' or form == 'off road' then - wp.action = 'Off Road' - elseif form == 'on_road' or form == 'on road' then - wp.action = 'On Road' - elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then - wp.action = 'Rank' - elseif form == 'cone' then - wp.action = 'Cone' - elseif form == 'diamond' then - wp.action = 'Diamond' - elseif form == 'vee' then - wp.action = 'Vee' - elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then - wp.action = 'EchelonL' - elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then - wp.action = 'EchelonR' - else - wp.action = 'Cone' -- if nothing matched - end - end - - wp.type = 'Turning Point' - - return wp - -end - -routines.fixedWing.buildWP = function(point, WPtype, speed, alt, altType) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - - if alt and type(alt) == 'number' then - wp.alt = alt - else - wp.alt = 2000 - end - - if altType then - altType = string.lower(altType) - if altType == 'radio' or 'agl' then - wp.alt_type = 'RADIO' - elseif altType == 'baro' or 'asl' then - wp.alt_type = 'BARO' - end - else - wp.alt_type = 'RADIO' - end - - if point.speed then - speed = point.speed - end - - if point.type then - WPtype = point.type - end - - if not speed then - wp.speed = routines.utils.kmphToMps(500) - else - wp.speed = speed - end - - if not WPtype then - wp.action = 'Turning Point' - else - WPtype = string.lower(WPtype) - if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then - wp.action = 'Fly Over Point' - elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then - wp.action = 'Turning Point' - else - wp.action = 'Turning Point' - end - end - - wp.type = 'Turning Point' - return wp -end - -routines.heli.buildWP = function(point, WPtype, speed, alt, altType) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - - if alt and type(alt) == 'number' then - wp.alt = alt - else - wp.alt = 500 - end - - if altType then - altType = string.lower(altType) - if altType == 'radio' or 'agl' then - wp.alt_type = 'RADIO' - elseif altType == 'baro' or 'asl' then - wp.alt_type = 'BARO' - end - else - wp.alt_type = 'RADIO' - end - - if point.speed then - speed = point.speed - end - - if point.type then - WPtype = point.type - end - - if not speed then - wp.speed = routines.utils.kmphToMps(200) - else - wp.speed = speed - end - - if not WPtype then - wp.action = 'Turning Point' - else - WPtype = string.lower(WPtype) - if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then - wp.action = 'Fly Over Point' - elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then - wp.action = 'Turning Point' - else - wp.action = 'Turning Point' - end - end - - wp.type = 'Turning Point' - return wp -end - -routines.groupToRandomPoint = function(vars) - local group = vars.group --Required - local point = vars.point --required - local radius = vars.radius or 0 - local innerRadius = vars.innerRadius - local form = vars.form or 'Cone' - local heading = vars.heading or math.random()*2*math.pi - local headingDegrees = vars.headingDegrees - local speed = vars.speed or routines.utils.kmphToMps(20) - - - local useRoads - if not vars.disableRoads then - useRoads = true - else - useRoads = false - end - - local path = {} - - if headingDegrees then - heading = headingDegrees*math.pi/180 - end - - if heading >= 2*math.pi then - heading = heading - 2*math.pi - end - - local rndCoord = routines.getRandPointInCircle(point, radius, innerRadius) - - local offset = {} - local posStart = routines.getLeadPos(group) - - offset.x = routines.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3) - offset.z = routines.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3) - path[#path + 1] = routines.ground.buildWP(posStart, form, speed) - - - if useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then - path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 11, ['z'] = posStart.z + 11}, 'off_road', speed) - path[#path + 1] = routines.ground.buildWP(posStart, 'on_road', speed) - path[#path + 1] = routines.ground.buildWP(offset, 'on_road', speed) - else - path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 25, ['z'] = posStart.z + 25}, form, speed) - end - - path[#path + 1] = routines.ground.buildWP(offset, form, speed) - path[#path + 1] = routines.ground.buildWP(rndCoord, form, speed) - - routines.goRoute(group, path) - - return -end - -routines.groupRandomDistSelf = function(gpData, dist, form, heading, speed) - local pos = routines.getLeadPos(gpData) - local fakeZone = {} - fakeZone.radius = dist or math.random(300, 1000) - fakeZone.point = {x = pos.x, y, pos.y, z = pos.z} - routines.groupToRandomZone(gpData, fakeZone, form, heading, speed) - - return -end - -routines.groupToRandomZone = function(gpData, zone, form, heading, speed) - if type(gpData) == 'string' then - gpData = Group.getByName(gpData) - end - - if type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - elseif type(zone) == 'table' and not zone.radius then - zone = trigger.misc.getZone(zone[math.random(1, #zone)]) - end - - if speed then - speed = routines.utils.kmphToMps(speed) - end - - local vars = {} - vars.group = gpData - vars.radius = zone.radius - vars.form = form - vars.headingDegrees = heading - vars.speed = speed - vars.point = routines.utils.zoneToVec3(zone) - - routines.groupToRandomPoint(vars) - - return -end - -routines.isTerrainValid = function(coord, terrainTypes) -- vec2/3 and enum or table of acceptable terrain types - if coord.z then - coord.y = coord.z - end - local typeConverted = {} - - if type(terrainTypes) == 'string' then -- if its a string it does this check - for constId, constData in pairs(land.SurfaceType) do - if string.lower(constId) == string.lower(terrainTypes) or string.lower(constData) == string.lower(terrainTypes) then - table.insert(typeConverted, constId) - end - end - elseif type(terrainTypes) == 'table' then -- if its a table it does this check - for typeId, typeData in pairs(terrainTypes) do - for constId, constData in pairs(land.SurfaceType) do - if string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeId) then - table.insert(typeConverted, constId) - end - end - end - end - for validIndex, validData in pairs(typeConverted) do - if land.getSurfaceType(coord) == land.SurfaceType[validData] then - return true - end - end - return false -end - -routines.groupToPoint = function(gpData, point, form, heading, speed, useRoads) - if type(point) == 'string' then - point = trigger.misc.getZone(point) - end - if speed then - speed = routines.utils.kmphToMps(speed) - end - - local vars = {} - vars.group = gpData - vars.form = form - vars.headingDegrees = heading - vars.speed = speed - vars.disableRoads = useRoads - vars.point = routines.utils.zoneToVec3(point) - routines.groupToRandomPoint(vars) - - return -end - - -routines.getLeadPos = function(group) - if type(group) == 'string' then -- group name - group = Group.getByName(group) - end - - local units = group:getUnits() - - local leader = units[1] - if not leader then -- SHOULD be good, but if there is a bug, this code future-proofs it then. - local lowestInd = math.huge - for ind, unit in pairs(units) do - if ind < lowestInd then - lowestInd = ind - leader = unit - end - end - end - if leader and Unit.isExist(leader) then -- maybe a little too paranoid now... - return leader:getPosition().p - end -end - ---[[ vars for routines.getMGRSString: -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer between 0 and 5, inclusive -]] -routines.getMGRSString = function(vars) - local units = vars.units - local acc = vars.acc or 5 - local avgPos = routines.getAvgPos(units) - if avgPos then - return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(avgPos)), acc) - end -end - ---[[ vars for routines.getLLString -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer, number of numbers after decimal place -vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. - - -]] -routines.getLLString = function(vars) - local units = vars.units - local acc = vars.acc or 3 - local DMS = vars.DMS - local avgPos = routines.getAvgPos(units) - if avgPos then - local lat, lon = coord.LOtoLL(avgPos) - return routines.tostringLL(lat, lon, acc, DMS) - end -end - ---[[ -vars.zone - table of a zone name. -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -]] -routines.getBRStringZone = function(vars) - local zone = trigger.misc.getZone( vars.zone ) - local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. - local alt = vars.alt - local metric = vars.metric - if zone then - local vec = {x = zone.point.x - ref.x, y = zone.point.y - ref.y, z = zone.point.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(zone.point, ref) - if alt then - alt = zone.y - end - return routines.tostringBR(dir, dist, alt, metric) - else - env.info( 'routines.getBRStringZone: error: zone is nil' ) - end -end - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -]] -routines.getBRString = function(vars) - local units = vars.units - local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. - local alt = vars.alt - local metric = vars.metric - local avgPos = routines.getAvgPos(units) - if avgPos then - local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(avgPos, ref) - if alt then - alt = avgPos.y - end - return routines.tostringBR(dir, dist, alt, metric) - end -end - - --- Returns the Vec3 coordinates of the average position of the concentration of units most in the heading direction. ---[[ vars for routines.getLeadingPos: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -]] -routines.getLeadingPos = function(vars) - local units = vars.units - local heading = vars.heading - local radius = vars.radius - if vars.headingDegrees then - heading = routines.utils.toRadian(vars.headingDegrees) - end - - local unitPosTbl = {} - for i = 1, #units do - local unit = Unit.getByName(units[i]) - if unit and unit:isExist() then - unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p - end - end - if #unitPosTbl > 0 then -- one more more units found. - -- first, find the unit most in the heading direction - local maxPos = -math.huge - - local maxPosInd -- maxPos - the furthest in direction defined by heading; maxPosInd = - for i = 1, #unitPosTbl do - local rotatedVec2 = routines.vec.rotateVec2(routines.utils.makeVec2(unitPosTbl[i]), heading) - if (not maxPos) or maxPos < rotatedVec2.x then - maxPos = rotatedVec2.x - maxPosInd = i - end - end - - --now, get all the units around this unit... - local avgPos - if radius then - local maxUnitPos = unitPosTbl[maxPosInd] - local avgx, avgy, avgz, totNum = 0, 0, 0, 0 - for i = 1, #unitPosTbl do - if routines.utils.get2DDist(maxUnitPos, unitPosTbl[i]) <= radius then - avgx = avgx + unitPosTbl[i].x - avgy = avgy + unitPosTbl[i].y - avgz = avgz + unitPosTbl[i].z - totNum = totNum + 1 - end - end - avgPos = { x = avgx/totNum, y = avgy/totNum, z = avgz/totNum} - else - avgPos = unitPosTbl[maxPosInd] - end - - return avgPos - end -end - - ---[[ vars for routines.getLeadingMGRSString: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.acc - number, 0 to 5. -]] -routines.getLeadingMGRSString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local acc = vars.acc or 5 - return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), acc) - end -end - ---[[ vars for routines.getLeadingLLString: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.acc - number of digits after decimal point (can be negative) -vars.DMS - boolean, true if you want DMS. -]] -routines.getLeadingLLString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local acc = vars.acc or 3 - local DMS = vars.DMS - local lat, lon = coord.LOtoLL(pos) - return routines.tostringLL(lat, lon, acc, DMS) - end -end - - - ---[[ vars for routines.getLeadingBRString: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.metric - boolean, if true, use km instead of NM. -vars.alt - boolean, if true, include altitude. -vars.ref - vec3/vec2 reference point. -]] -routines.getLeadingBRString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local ref = vars.ref - local alt = vars.alt - local metric = vars.metric - - local vec = {x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(pos, ref) - if alt then - alt = pos.y - end - return routines.tostringBR(dir, dist, alt, metric) - end -end - ---[[ vars for routines.message.add - vars.text = 'Hello World' - vars.displayTime = 20 - vars.msgFor = {coa = {'red'}, countries = {'Ukraine', 'Georgia'}, unitTypes = {'A-10C'}} - -]] - ---[[ vars for routines.msgMGRS -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer between 0 and 5, inclusive -vars.text - text in the message -vars.displayTime - self explanatory -vars.msgFor - scope -]] -routines.msgMGRS = function(vars) - local units = vars.units - local acc = vars.acc - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getMGRSString{units = units, acc = acc} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } -end - ---[[ vars for routines.msgLL -vars.units - table of unit names (NOT unitNameTable- maybe this should change) (Yes). -vars.acc - integer, number of numbers after decimal place -vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. -vars.text - text in the message -vars.displayTime - self explanatory -vars.msgFor - scope -]] -routines.msgLL = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local acc = vars.acc - local DMS = vars.DMS - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLLString{units = units, acc = acc, DMS = DMS} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgBR = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString - local alt = vars.alt - local metric = vars.metric - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getBRString{units = units, ref = ref, alt = alt, metric = metric} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - - --------------------------------------------------------------------------------------------- --- basically, just sub-types of routines.msgBR... saves folks the work of getting the ref point. ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - string red, blue -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgBullseye = function(vars) - if string.lower(vars.ref) == 'red' then - vars.ref = routines.DBs.missionData.bullseye.red - routines.msgBR(vars) - elseif string.lower(vars.ref) == 'blue' then - vars.ref = routines.DBs.missionData.bullseye.blue - routines.msgBR(vars) - end -end - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - unit name of reference point -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] - -routines.msgBRA = function(vars) - if Unit.getByName(vars.ref) then - vars.ref = Unit.getByName(vars.ref):getPosition().p - if not vars.alt then - vars.alt = true - end - routines.msgBR(vars) - end -end --------------------------------------------------------------------------------------------- - ---[[ vars for routines.msgLeadingMGRS: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.acc - number, 0 to 5. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingMGRS = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local acc = vars.acc - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingMGRSString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - - -end ---[[ vars for routines.msgLeadingLL: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.acc - number of digits after decimal point (can be negative) -vars.DMS - boolean, true if you want DMS. (optional) -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingLL = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local acc = vars.acc - local DMS = vars.DMS - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingLLString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - ---[[ -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.metric - boolean, if true, use km instead of NM. (optional) -vars.alt - boolean, if true, include altitude. (optional) -vars.ref - vec3/vec2 reference point. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingBR = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local metric = vars.metric - local alt = vars.alt - local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingBRString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } -end - - -function spairs(t, order) - -- collect the keys - local keys = {} - for k in pairs(t) do keys[#keys+1] = k end - - -- if order function given, sort by it by passing the table and keys a, b, - -- otherwise just sort the keys - if order then - table.sort(keys, function(a,b) return order(t, a, b) end) - else - table.sort(keys) - end - - -- return the iterator function - local i = 0 - return function() - i = i + 1 - if keys[i] then - return keys[i], t[keys[i]] - end - end -end - - -function routines.IsPartOfGroupInZones( CargoGroup, LandingZones ) ---trace.f() - - local CurrentZoneID = nil - - if CargoGroup then - local CargoUnits = CargoGroup:getUnits() - for CargoUnitID, CargoUnit in pairs( CargoUnits ) do - if CargoUnit and CargoUnit:getLife() >= 1.0 then - CurrentZoneID = routines.IsUnitInZones( CargoUnit, LandingZones ) - if CurrentZoneID then - break - end - end - end - end - ---trace.r( "", "", { CurrentZoneID } ) - return CurrentZoneID -end - - - -function routines.IsUnitInZones( TransportUnit, LandingZones ) ---trace.f("", "routines.IsUnitInZones" ) - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - if TransportUnit then - local TransportUnitPos = TransportUnit:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = 1 - end - end - if TransportZoneResult then - --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) - else - --trace.i( "routines", "TransportZone:nil logic" ) - end - return TransportZoneResult - else - --trace.i( "routines", "TransportZone:nil hard" ) - return nil - end -end - -function routines.IsUnitNearZonesRadius( TransportUnit, LandingZones, ZoneRadius ) ---trace.f("", "routines.IsUnitInZones" ) - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - if TransportUnit then - local TransportUnitPos = TransportUnit:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then - TransportZoneResult = 1 - end - end - if TransportZoneResult then - --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) - else - --trace.i( "routines", "TransportZone:nil logic" ) - end - return TransportZoneResult - else - --trace.i( "routines", "TransportZone:nil hard" ) - return nil - end -end - - -function routines.IsStaticInZones( TransportStatic, LandingZones ) ---trace.f() - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - local TransportStaticPos = TransportStatic:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = 1 - end - end - ---trace.r( "", "", { TransportZoneResult } ) - return TransportZoneResult -end - - -function routines.IsUnitInRadius( CargoUnit, ReferencePosition, Radius ) ---trace.f() - - local Valid = true - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - local CargoPos = CargoUnit:getPosition().p - local ReferenceP = ReferencePosition.p - - if (((CargoPos.x - ReferenceP.x)^2 + (CargoPos.z - ReferenceP.z)^2)^0.5 <= Radius) then - else - Valid = false - end - - return Valid -end - -function routines.IsPartOfGroupInRadius( CargoGroup, ReferencePosition, Radius ) ---trace.f() - - local Valid = true - - Valid = routines.ValidateGroup( CargoGroup, "CargoGroup", Valid ) - - -- fill-up some local variables to support further calculations to determine location of units within the zone - local CargoUnits = CargoGroup:getUnits() - for CargoUnitId, CargoUnit in pairs( CargoUnits ) do - local CargoUnitPos = CargoUnit:getPosition().p --- env.info( 'routines.IsPartOfGroupInRadius: CargoUnitPos.x = ' .. CargoUnitPos.x .. ' CargoUnitPos.z = ' .. CargoUnitPos.z ) - local ReferenceP = ReferencePosition.p --- env.info( 'routines.IsPartOfGroupInRadius: ReferenceGroupPos.x = ' .. ReferenceGroupPos.x .. ' ReferenceGroupPos.z = ' .. ReferenceGroupPos.z ) - - if ((( CargoUnitPos.x - ReferenceP.x)^2 + (CargoUnitPos.z - ReferenceP.z)^2)^0.5 <= Radius) then - else - Valid = false - break - end - end - - return Valid -end - - -function routines.ValidateString( Variable, VariableName, Valid ) ---trace.f() - - if type( Variable ) == "string" then - if Variable == "" then - error( "routines.ValidateString: error: " .. VariableName .. " must be filled out!" ) - Valid = false - end - else - error( "routines.ValidateString: error: " .. VariableName .. " is not a string." ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateNumber( Variable, VariableName, Valid ) ---trace.f() - - if type( Variable ) == "number" then - else - error( "routines.ValidateNumber: error: " .. VariableName .. " is not a number." ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid - -end - -function routines.ValidateGroup( Variable, VariableName, Valid ) ---trace.f() - - if Variable == nil then - error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateZone( LandingZones, VariableName, Valid ) ---trace.f() - - if LandingZones == nil then - error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) - Valid = false - end - - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - if trigger.misc.getZone( LandingZoneName ) == nil then - error( "routines.ValidateGroup: error: Zone " .. LandingZoneName .. " does not exist!" ) - Valid = false - break - end - end - else - if trigger.misc.getZone( LandingZones ) == nil then - error( "routines.ValidateGroup: error: Zone " .. LandingZones .. " does not exist!" ) - Valid = false - end - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateEnumeration( Variable, VariableName, Enum, Valid ) ---trace.f() - - local ValidVariable = false - - for EnumId, EnumData in pairs( Enum ) do - if Variable == EnumData then - ValidVariable = true - break - end - end - - if ValidVariable then - else - error( 'TransportValidateEnum: " .. VariableName .. " is not a valid type.' .. Variable ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.getGroupRoute(groupIdent, task) -- same as getGroupPoints but returns speed and formation type along with vec2 of point} - -- refactor to search by groupId and allow groupId and groupName as inputs - local gpId = groupIdent - if type(groupIdent) == 'string' and not tonumber(groupIdent) then - gpId = _DATABASE.Templates.Groups[groupIdent].groupId - 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 - if group_data.route and group_data.route.points and #group_data.route.points > 0 then - local points = {} - - for point_num, point in pairs(group_data.route.points) do - local routeData = {} - if not point.point then - routeData.x = point.x - routeData.y = point.y - else - routeData.point = point.point --it's possible that the ME could move to the point = Vec2 notation. - end - routeData.form = point.action - routeData.speed = point.speed - routeData.alt = point.alt - routeData.alt_type = point.alt_type - routeData.airdromeId = point.airdromeId - routeData.helipadId = point.helipadId - routeData.type = point.type - routeData.action = point.action - if task then - routeData.task = point.task - end - points[point_num] = routeData - end - - return points - end - return - end --if group_data and group_data.name and group_data.name == 'groupname' - end --for group_num, group_data in pairs(obj_type_data.group) do - end --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 - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do -end - -routines.ground.patrolRoute = function(vars) - - - local tempRoute = {} - local useRoute = {} - local gpData = vars.gpData - if type(gpData) == 'string' then - gpData = Group.getByName(gpData) - end - - local useGroupRoute - if not vars.useGroupRoute then - useGroupRoute = vars.gpData - else - useGroupRoute = vars.useGroupRoute - end - local routeProvided = false - if not vars.route then - if useGroupRoute then - tempRoute = routines.getGroupRoute(useGroupRoute) - end - else - useRoute = vars.route - local posStart = routines.getLeadPos(gpData) - useRoute[1] = routines.ground.buildWP(posStart, useRoute[1].action, useRoute[1].speed) - routeProvided = true - end - - - local overRideSpeed = vars.speed or 'default' - local pType = vars.pType - local offRoadForm = vars.offRoadForm or 'default' - local onRoadForm = vars.onRoadForm or 'default' - - if routeProvided == false and #tempRoute > 0 then - local posStart = routines.getLeadPos(gpData) - - - useRoute[#useRoute + 1] = routines.ground.buildWP(posStart, offRoadForm, overRideSpeed) - for i = 1, #tempRoute do - local tempForm = tempRoute[i].action - local tempSpeed = tempRoute[i].speed - - if offRoadForm == 'default' then - tempForm = tempRoute[i].action - end - if onRoadForm == 'default' then - onRoadForm = 'On Road' - end - if (string.lower(tempRoute[i].action) == 'on road' or string.lower(tempRoute[i].action) == 'onroad' or string.lower(tempRoute[i].action) == 'on_road') then - tempForm = onRoadForm - else - tempForm = offRoadForm - end - - if type(overRideSpeed) == 'number' then - tempSpeed = overRideSpeed - end - - - useRoute[#useRoute + 1] = routines.ground.buildWP(tempRoute[i], tempForm, tempSpeed) - end - - if pType and string.lower(pType) == 'doubleback' then - local curRoute = routines.utils.deepCopy(useRoute) - for i = #curRoute, 2, -1 do - useRoute[#useRoute + 1] = routines.ground.buildWP(curRoute[i], curRoute[i].action, curRoute[i].speed) - end - end - - useRoute[1].action = useRoute[#useRoute].action -- make it so the first WP matches the last WP - end - - local cTask3 = {} - local newPatrol = {} - newPatrol.route = useRoute - newPatrol.gpData = gpData:getName() - cTask3[#cTask3 + 1] = 'routines.ground.patrolRoute(' - cTask3[#cTask3 + 1] = routines.utils.oneLineSerialize(newPatrol) - cTask3[#cTask3 + 1] = ')' - cTask3 = table.concat(cTask3) - local tempTask = { - id = 'WrappedAction', - params = { - action = { - id = 'Script', - params = { - command = cTask3, - - }, - }, - }, - } - - - useRoute[#useRoute].task = tempTask - routines.goRoute(gpData, useRoute) - - return -end - -routines.ground.patrol = function(gpData, pType, form, speed) - local vars = {} - - if type(gpData) == 'table' and gpData:getName() then - gpData = gpData:getName() - end - - vars.useGroupRoute = gpData - vars.gpData = gpData - vars.pType = pType - vars.offRoadForm = form - vars.speed = speed - - routines.ground.patrolRoute(vars) - - return -end - -function routines.GetUnitHeight( CheckUnit ) ---trace.f( "routines" ) - - local UnitPoint = CheckUnit:getPoint() - local UnitPosition = { x = UnitPoint.x, y = UnitPoint.z } - local UnitHeight = UnitPoint.y - - local LandHeight = land.getHeight( UnitPosition ) - - --env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) - - --trace.f( "routines", "Unit Height = " .. UnitHeight - LandHeight ) - - return UnitHeight - LandHeight - -end - - - -Su34Status = { status = {} } -boardMsgRed = { statusMsg = "" } -boardMsgAll = { timeMsg = "" } -SpawnSettings = {} -Su34MenuPath = {} -Su34Menus = 0 - - -function Su34AttackCarlVinson(groupName) ---trace.menu("", "Su34AttackCarlVinson") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34.getController(groupSu34) - local groupCarlVinson = Group.getByName("US Carl Vinson #001") - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - if groupCarlVinson ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupCarlVinson:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) - end - Su34Status.status[groupName] = 1 - MessageToRed( string.format('%s: ',groupName) .. 'Attacking carrier Carl Vinson. ', 10, 'RedStatus' .. groupName ) -end - -function Su34AttackWest(groupName) ---trace.f("","Su34AttackWest") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34.getController(groupSu34) - local groupShipWest1 = Group.getByName("US Ship West #001") - local groupShipWest2 = Group.getByName("US Ship West #002") - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - if groupShipWest1 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipWest1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) - end - if groupShipWest2 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipWest2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) - end - Su34Status.status[groupName] = 2 - MessageToRed( string.format('%s: ',groupName) .. 'Attacking invading ships in the west. ', 10, 'RedStatus' .. groupName ) -end - -function Su34AttackNorth(groupName) ---trace.menu("","Su34AttackNorth") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34.getController(groupSu34) - local groupShipNorth1 = Group.getByName("US Ship North #001") - local groupShipNorth2 = Group.getByName("US Ship North #002") - local groupShipNorth3 = Group.getByName("US Ship North #003") - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - if groupShipNorth1 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) - end - if groupShipNorth2 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) - end - if groupShipNorth3 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth3:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) - end - Su34Status.status[groupName] = 3 - MessageToRed( string.format('%s: ',groupName) .. 'Attacking invading ships in the north. ', 10, 'RedStatus' .. groupName ) -end - -function Su34Orbit(groupName) ---trace.menu("","Su34Orbit") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34:getController() - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - controllerSu34:pushTask( {id = 'ControlledTask', params = { task = { id = 'Orbit', params = { pattern = AI.Task.OrbitPattern.RACE_TRACK } }, stopCondition = { duration = 600 } } } ) - Su34Status.status[groupName] = 4 - MessageToRed( string.format('%s: ',groupName) .. 'In orbit and awaiting further instructions. ', 10, 'RedStatus' .. groupName ) -end - -function Su34TakeOff(groupName) ---trace.menu("","Su34TakeOff") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34:getController() - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) - Su34Status.status[groupName] = 8 - MessageToRed( string.format('%s: ',groupName) .. 'Take-Off. ', 10, 'RedStatus' .. groupName ) -end - -function Su34Hold(groupName) ---trace.menu("","Su34Hold") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34:getController() - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) - Su34Status.status[groupName] = 5 - MessageToRed( string.format('%s: ',groupName) .. 'Holding Weapons. ', 10, 'RedStatus' .. groupName ) -end - -function Su34RTB(groupName) ---trace.menu("","Su34RTB") - Su34Status.status[groupName] = 6 - MessageToRed( string.format('%s: ',groupName) .. 'Return to Krasnodar. ', 10, 'RedStatus' .. groupName ) -end - -function Su34Destroyed(groupName) ---trace.menu("","Su34Destroyed") - Su34Status.status[groupName] = 7 - MessageToRed( string.format('%s: ',groupName) .. 'Destroyed. ', 30, 'RedStatus' .. groupName ) -end - -function GroupAlive( groupName ) ---trace.menu("","GroupAlive") - local groupTest = Group.getByName( groupName ) - - local groupExists = false - - if groupTest then - groupExists = groupTest:isExist() - end - - --trace.r( "", "", { groupExists } ) - return groupExists -end - -function Su34IsDead() ---trace.f() - -end - -function Su34OverviewStatus() ---trace.menu("","Su34OverviewStatus") - local msg = "" - local currentStatus = 0 - local Exists = false - - for groupName, currentStatus in pairs(Su34Status.status) do - - env.info(('Su34 Overview Status: GroupName = ' .. groupName )) - Alive = GroupAlive( groupName ) - - if Alive then - if currentStatus == 1 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Attacking carrier Carl Vinson. " - elseif currentStatus == 2 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Attacking supporting ships in the west. " - elseif currentStatus == 3 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Attacking invading ships in the north. " - elseif currentStatus == 4 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "In orbit and awaiting further instructions. " - elseif currentStatus == 5 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Holding Weapons. " - elseif currentStatus == 6 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Return to Krasnodar. " - elseif currentStatus == 7 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Destroyed. " - elseif currentStatus == 8 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Take-Off. " - end - else - if currentStatus == 7 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Destroyed. " - else - Su34Destroyed(groupName) - end - end - end - - boardMsgRed.statusMsg = msg -end - - -function UpdateBoardMsg() ---trace.f() - Su34OverviewStatus() - MessageToRed( boardMsgRed.statusMsg, 15, 'RedStatus' ) -end - -function MusicReset( flg ) ---trace.f() - trigger.action.setUserFlag(95,flg) -end - -function PlaneActivate(groupNameFormat, flg) ---trace.f() - local groupName = groupNameFormat .. string.format("#%03d", trigger.misc.getUserFlag(flg)) - --trigger.action.outText(groupName,10) - trigger.action.activateGroup(Group.getByName(groupName)) -end - -function Su34Menu(groupName) ---trace.f() - - --env.info(( 'Su34Menu(' .. groupName .. ')' )) - local groupSu34 = Group.getByName( groupName ) - - if Su34Status.status[groupName] == 1 or - Su34Status.status[groupName] == 2 or - Su34Status.status[groupName] == 3 or - Su34Status.status[groupName] == 4 or - Su34Status.status[groupName] == 5 then - if Su34MenuPath[groupName] == nil then - if planeMenuPath == nil then - planeMenuPath = missionCommands.addSubMenuForCoalition( - coalition.side.RED, - "SU-34 anti-ship flights", - nil - ) - end - Su34MenuPath[groupName] = missionCommands.addSubMenuForCoalition( - coalition.side.RED, - "Flight " .. groupName, - planeMenuPath - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Attack carrier Carl Vinson", - Su34MenuPath[groupName], - Su34AttackCarlVinson, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Attack ships in the west", - Su34MenuPath[groupName], - Su34AttackWest, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Attack ships in the north", - Su34MenuPath[groupName], - Su34AttackNorth, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Hold position and await instructions", - Su34MenuPath[groupName], - Su34Orbit, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Report status", - Su34MenuPath[groupName], - Su34OverviewStatus - ) - end - else - if Su34MenuPath[groupName] then - missionCommands.removeItemForCoalition(coalition.side.RED, Su34MenuPath[groupName]) - end - end -end - ---- Obsolete function, but kept to rework in framework. - -function ChooseInfantry ( TeleportPrefixTable, TeleportMax ) ---trace.f("Spawn") - --env.info(( 'ChooseInfantry: ' )) - - TeleportPrefixTableCount = #TeleportPrefixTable - TeleportPrefixTableIndex = math.random( 1, TeleportPrefixTableCount ) - - --env.info(( 'ChooseInfantry: TeleportPrefixTableIndex = ' .. TeleportPrefixTableIndex .. ' TeleportPrefixTableCount = ' .. TeleportPrefixTableCount .. ' TeleportMax = ' .. TeleportMax )) - - local TeleportFound = false - local TeleportLoop = true - local Index = TeleportPrefixTableIndex - local TeleportPrefix = '' - - while TeleportLoop do - TeleportPrefix = TeleportPrefixTable[Index] - if SpawnSettings[TeleportPrefix] then - if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then - SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1 - TeleportFound = true - else - TeleportFound = false - end - else - SpawnSettings[TeleportPrefix] = {} - SpawnSettings[TeleportPrefix]['SpawnCount'] = 0 - TeleportFound = true - end - if TeleportFound then - TeleportLoop = false - else - if Index < TeleportPrefixTableCount then - Index = Index + 1 - else - TeleportLoop = false - end - end - --env.info(( 'ChooseInfantry: Loop 1 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index )) - end - - if TeleportFound == false then - TeleportLoop = true - Index = 1 - while TeleportLoop do - TeleportPrefix = TeleportPrefixTable[Index] - if SpawnSettings[TeleportPrefix] then - if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then - SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1 - TeleportFound = true - else - TeleportFound = false - end - else - SpawnSettings[TeleportPrefix] = {} - SpawnSettings[TeleportPrefix]['SpawnCount'] = 0 - TeleportFound = true - end - if TeleportFound then - TeleportLoop = false - else - if Index < TeleportPrefixTableIndex then - Index = Index + 1 - else - TeleportLoop = false - end - end - --env.info(( 'ChooseInfantry: Loop 2 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index )) - end - end - - local TeleportGroupName = '' - if TeleportFound == true then - TeleportGroupName = TeleportPrefix .. string.format("#%03d", SpawnSettings[TeleportPrefix]['SpawnCount'] ) - else - TeleportGroupName = '' - end - - --env.info(('ChooseInfantry: TeleportGroupName = ' .. TeleportGroupName )) - --env.info(('ChooseInfantry: return')) - - return TeleportGroupName -end - -SpawnedInfantry = 0 - -function LandCarrier ( CarrierGroup, LandingZonePrefix ) ---trace.f() - --env.info(( 'LandCarrier: ' )) - --env.info(( 'LandCarrier: CarrierGroup = ' .. CarrierGroup:getName() )) - --env.info(( 'LandCarrier: LandingZone = ' .. LandingZonePrefix )) - - local controllerGroup = CarrierGroup:getController() - - local LandingZone = trigger.misc.getZone(LandingZonePrefix) - local LandingZonePos = {} - LandingZonePos.x = LandingZone.point.x + math.random(LandingZone.radius * -1, LandingZone.radius) - LandingZonePos.y = LandingZone.point.z + math.random(LandingZone.radius * -1, LandingZone.radius) - - controllerGroup:pushTask( { id = 'Land', params = { point = LandingZonePos, durationFlag = true, duration = 10 } } ) - - --env.info(( 'LandCarrier: end' )) -end - -EscortCount = 0 -function EscortCarrier ( CarrierGroup, EscortPrefix, EscortLastWayPoint, EscortEngagementDistanceMax, EscortTargetTypes ) ---trace.f() - --env.info(( 'EscortCarrier: ' )) - --env.info(( 'EscortCarrier: CarrierGroup = ' .. CarrierGroup:getName() )) - --env.info(( 'EscortCarrier: EscortPrefix = ' .. EscortPrefix )) - - local CarrierName = CarrierGroup:getName() - - local EscortMission = {} - local CarrierMission = {} - - local EscortMission = SpawnMissionGroup( EscortPrefix ) - local CarrierMission = SpawnMissionGroup( CarrierGroup:getName() ) - - if EscortMission ~= nil and CarrierMission ~= nil then - - EscortCount = EscortCount + 1 - EscortMissionName = string.format( EscortPrefix .. '#Escort %s', CarrierName ) - EscortMission.name = EscortMissionName - EscortMission.groupId = nil - EscortMission.lateActivation = false - EscortMission.taskSelected = false - - local EscortUnits = #EscortMission.units - for u = 1, EscortUnits do - EscortMission.units[u].name = string.format( EscortPrefix .. '#Escort %s %02d', CarrierName, u ) - EscortMission.units[u].unitId = nil - end - - - EscortMission.route.points[1].task = { id = "ComboTask", - params = - { - tasks = - { - [1] = - { - enabled = true, - auto = false, - id = "Escort", - number = 1, - params = - { - lastWptIndexFlagChangedManually = false, - groupId = CarrierGroup:getID(), - lastWptIndex = nil, - lastWptIndexFlag = false, - engagementDistMax = EscortEngagementDistanceMax, - targetTypes = EscortTargetTypes, - pos = - { - y = 20, - x = 20, - z = 0, - } -- end of ["pos"] - } -- end of ["params"] - } -- end of [1] - } -- end of ["tasks"] - } -- end of ["params"] - } -- end of ["task"] - - SpawnGroupAdd( EscortPrefix, EscortMission ) - - end -end - -function SendMessageToCarrier( CarrierGroup, CarrierMessage ) ---trace.f() - - if CarrierGroup ~= nil then - MessageToGroup( CarrierGroup, CarrierMessage, 30, 'Carrier/' .. CarrierGroup:getName() ) - end - -end - -function MessageToGroup( MsgGroup, MsgText, MsgTime, MsgName ) ---trace.f() - - if type(MsgGroup) == 'string' then - --env.info( 'MessageToGroup: Converted MsgGroup string "' .. MsgGroup .. '" into a Group structure.' ) - MsgGroup = Group.getByName( MsgGroup ) - end - - if MsgGroup ~= nil then - local MsgTable = {} - MsgTable.text = MsgText - MsgTable.displayTime = MsgTime - MsgTable.msgFor = { units = { MsgGroup:getUnits()[1]:getName() } } - MsgTable.name = MsgName - --routines.message.add( MsgTable ) - --env.info(('MessageToGroup: Message sent to ' .. MsgGroup:getUnits()[1]:getName() .. ' -> ' .. MsgText )) - end -end - -function MessageToUnit( UnitName, MsgText, MsgTime, MsgName ) ---trace.f() - - if UnitName ~= nil then - local MsgTable = {} - MsgTable.text = MsgText - MsgTable.displayTime = MsgTime - MsgTable.msgFor = { units = { UnitName } } - MsgTable.name = MsgName - --routines.message.add( MsgTable ) - end -end - -function MessageToAll( MsgText, MsgTime, MsgName ) ---trace.f() - - MESSAGE:New( MsgText, MsgTime, "Message" ):ToCoalition( coalition.side.RED ):ToCoalition( coalition.side.BLUE ) -end - -function MessageToRed( MsgText, MsgTime, MsgName ) ---trace.f() - - MESSAGE:New( MsgText, MsgTime, "To Red Coalition" ):ToCoalition( coalition.side.RED ) -end - -function MessageToBlue( MsgText, MsgTime, MsgName ) ---trace.f() - - MESSAGE:New( MsgText, MsgTime, "To Blue Coalition" ):ToCoalition( coalition.side.RED ) -end - -function getCarrierHeight( CarrierGroup ) ---trace.f() - - if CarrierGroup ~= nil then - if table.getn(CarrierGroup:getUnits()) == 1 then - local CarrierUnit = CarrierGroup:getUnits()[1] - local CurrentPoint = CarrierUnit:getPoint() - - local CurrentPosition = { x = CurrentPoint.x, y = CurrentPoint.z } - local CarrierHeight = CurrentPoint.y - - local LandHeight = land.getHeight( CurrentPosition ) - - --env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) - - return CarrierHeight - LandHeight - else - return 999999 - end - else - return 999999 - end - -end - -function GetUnitHeight( CheckUnit ) ---trace.f() - - local UnitPoint = CheckUnit:getPoint() - local UnitPosition = { x = CurrentPoint.x, y = CurrentPoint.z } - local UnitHeight = CurrentPoint.y - - local LandHeight = land.getHeight( CurrentPosition ) - - --env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) - - return UnitHeight - LandHeight - -end - - -_MusicTable = {} -_MusicTable.Files = {} -_MusicTable.Queue = {} -_MusicTable.FileCnt = 0 - - -function MusicRegister( SndRef, SndFile, SndTime ) ---trace.f() - - env.info(( 'MusicRegister: SndRef = ' .. SndRef )) - env.info(( 'MusicRegister: SndFile = ' .. SndFile )) - env.info(( 'MusicRegister: SndTime = ' .. SndTime )) - - - _MusicTable.FileCnt = _MusicTable.FileCnt + 1 - - _MusicTable.Files[_MusicTable.FileCnt] = {} - _MusicTable.Files[_MusicTable.FileCnt].Ref = SndRef - _MusicTable.Files[_MusicTable.FileCnt].File = SndFile - _MusicTable.Files[_MusicTable.FileCnt].Time = SndTime - - if not _MusicTable.Function then - _MusicTable.Function = routines.scheduleFunction( MusicScheduler, { }, timer.getTime() + 10, 10) - end - -end - -function MusicToPlayer( SndRef, PlayerName, SndContinue ) ---trace.f() - - --env.info(( 'MusicToPlayer: SndRef = ' .. SndRef )) - - local PlayerUnits = AlivePlayerUnits() - for PlayerUnitIdx, PlayerUnit in pairs(PlayerUnits) do - local PlayerUnitName = PlayerUnit:getPlayerName() - --env.info(( 'MusicToPlayer: PlayerUnitName = ' .. PlayerUnitName )) - if PlayerName == PlayerUnitName then - PlayerGroup = PlayerUnit:getGroup() - if PlayerGroup then - --env.info(( 'MusicToPlayer: PlayerGroup = ' .. PlayerGroup:getName() )) - MusicToGroup( SndRef, PlayerGroup, SndContinue ) - end - break - end - end - - --env.info(( 'MusicToPlayer: end' )) - -end - -function MusicToGroup( SndRef, SndGroup, SndContinue ) ---trace.f() - - --env.info(( 'MusicToGroup: SndRef = ' .. SndRef )) - - if SndGroup ~= nil then - if _MusicTable and _MusicTable.FileCnt > 0 then - if SndGroup:isExist() then - if MusicCanStart(SndGroup:getUnit(1):getPlayerName()) then - --env.info(( 'MusicToGroup: OK for Sound.' )) - local SndIdx = 0 - if SndRef == '' then - --env.info(( 'MusicToGroup: SndRef as empty. Queueing at random.' )) - SndIdx = math.random( 1, _MusicTable.FileCnt ) - else - for SndIdx = 1, _MusicTable.FileCnt do - if _MusicTable.Files[SndIdx].Ref == SndRef then - break - end - end - end - --env.info(( 'MusicToGroup: SndIdx = ' .. SndIdx )) - --env.info(( 'MusicToGroup: Queueing Music ' .. _MusicTable.Files[SndIdx].File .. ' for Group ' .. SndGroup:getID() )) - trigger.action.outSoundForGroup( SndGroup:getID(), _MusicTable.Files[SndIdx].File ) - MessageToGroup( SndGroup, 'Playing ' .. _MusicTable.Files[SndIdx].File, 15, 'Music-' .. SndGroup:getUnit(1):getPlayerName() ) - - local SndQueueRef = SndGroup:getUnit(1):getPlayerName() - if _MusicTable.Queue[SndQueueRef] == nil then - _MusicTable.Queue[SndQueueRef] = {} - end - _MusicTable.Queue[SndQueueRef].Start = timer.getTime() - _MusicTable.Queue[SndQueueRef].PlayerName = SndGroup:getUnit(1):getPlayerName() - _MusicTable.Queue[SndQueueRef].Group = SndGroup - _MusicTable.Queue[SndQueueRef].ID = SndGroup:getID() - _MusicTable.Queue[SndQueueRef].Ref = SndIdx - _MusicTable.Queue[SndQueueRef].Continue = SndContinue - _MusicTable.Queue[SndQueueRef].Type = Group - end - end - end - end -end - -function MusicCanStart(PlayerName) ---trace.f() - - --env.info(( 'MusicCanStart:' )) - - local MusicOut = false - - if _MusicTable['Queue'] ~= nil and _MusicTable.FileCnt > 0 then - --env.info(( 'MusicCanStart: PlayerName = ' .. PlayerName )) - local PlayerFound = false - local MusicStart = 0 - local MusicTime = 0 - for SndQueueIdx, SndQueue in pairs( _MusicTable.Queue ) do - if SndQueue.PlayerName == PlayerName then - PlayerFound = true - MusicStart = SndQueue.Start - MusicTime = _MusicTable.Files[SndQueue.Ref].Time - break - end - end - if PlayerFound then - --env.info(( 'MusicCanStart: MusicStart = ' .. MusicStart )) - --env.info(( 'MusicCanStart: MusicTime = ' .. MusicTime )) - --env.info(( 'MusicCanStart: timer.getTime() = ' .. timer.getTime() )) - - if MusicStart + MusicTime <= timer.getTime() then - MusicOut = true - end - else - MusicOut = true - end - end - - if MusicOut then - --env.info(( 'MusicCanStart: true' )) - else - --env.info(( 'MusicCanStart: false' )) - end - - return MusicOut -end - -function MusicScheduler() ---trace.scheduled("", "MusicScheduler") - - --env.info(( 'MusicScheduler:' )) - if _MusicTable['Queue'] ~= nil and _MusicTable.FileCnt > 0 then - --env.info(( 'MusicScheduler: Walking Sound Queue.')) - for SndQueueIdx, SndQueue in pairs( _MusicTable.Queue ) do - if SndQueue.Continue then - if MusicCanStart(SndQueue.PlayerName) then - --env.info(('MusicScheduler: MusicToGroup')) - MusicToPlayer( '', SndQueue.PlayerName, true ) - end - end - end - end - -end - - -env.info(( 'Init: Scripts Loaded v1.1' )) - ---- This module contains derived utilities taken from the MIST framework, --- which are excellent tools to be reused in an OO environment!. --- --- ### Authors: --- --- * Grimes : Design & Programming of the MIST framework. --- --- ### Contributions: --- --- * FlightControl : Rework to OO framework --- --- @module Utils - - ---- @type SMOKECOLOR --- @field Green --- @field Red --- @field White --- @field Orange --- @field Blue - -SMOKECOLOR = trigger.smokeColor -- #SMOKECOLOR - ---- @type FLARECOLOR --- @field Green --- @field Red --- @field White --- @field Yellow - -FLARECOLOR = trigger.flareColor -- #FLARECOLOR - ---- Utilities static class. --- @type UTILS -UTILS = { - _MarkID = 1 -} - ---- Function to infer instance of an object --- --- ### Examples: --- --- * UTILS.IsInstanceOf( 'some text', 'string' ) will return true --- * UTILS.IsInstanceOf( some_function, 'function' ) will return true --- * UTILS.IsInstanceOf( 10, 'number' ) will return true --- * UTILS.IsInstanceOf( false, 'boolean' ) will return true --- * UTILS.IsInstanceOf( nil, 'nil' ) will return true --- --- * UTILS.IsInstanceOf( ZONE:New( 'some zone', ZONE ) will return true --- * UTILS.IsInstanceOf( ZONE:New( 'some zone', 'ZONE' ) will return true --- * UTILS.IsInstanceOf( ZONE:New( 'some zone', 'zone' ) will return true --- * UTILS.IsInstanceOf( ZONE:New( 'some zone', 'BASE' ) will return true --- --- * UTILS.IsInstanceOf( ZONE:New( 'some zone', 'GROUP' ) will return false --- --- --- @param object is the object to be evaluated --- @param className is the name of the class to evaluate (can be either a string or a Moose class) --- @return #boolean -UTILS.IsInstanceOf = function( object, className ) - -- Is className NOT a string ? - if not type( className ) == 'string' then - - -- Is className a Moose class ? - if type( className ) == 'table' and className.IsInstanceOf ~= nil then - - -- Get the name of the Moose class as a string - className = className.ClassName - - -- className is neither a string nor a Moose class, throw an error - else - - -- I'm not sure if this should take advantage of MOOSE logging function, or throw an error for pcall - local err_str = 'className parameter should be a string; parameter received: '..type( className ) - self:E( err_str ) - return false - -- error( err_str ) - - end - end - - -- Is the object a Moose class instance ? - if type( object ) == 'table' and object.IsInstanceOf ~= nil then - - -- Use the IsInstanceOf method of the BASE class - return object:IsInstanceOf( className ) - else - - -- If the object is not an instance of a Moose class, evaluate against lua basic data types - local basicDataTypes = { 'string', 'number', 'function', 'boolean', 'nil', 'table' } - for _, basicDataType in ipairs( basicDataTypes ) do - if className == basicDataType then - return type( object ) == basicDataType - end - end - end - - -- Check failed - return false -end - - ---from http://lua-users.org/wiki/CopyTable -UTILS.DeepCopy = function(object) - local lookup_table = {} - local function _copy(object) - if type(object) ~= "table" then - return object - elseif lookup_table[object] then - return lookup_table[object] - end - local new_table = {} - lookup_table[object] = new_table - for index, value in pairs(object) do - new_table[_copy(index)] = _copy(value) - end - return setmetatable(new_table, getmetatable(object)) - end - local objectreturn = _copy(object) - return objectreturn -end - - --- porting in Slmod's serialize_slmod2 -UTILS.OneLineSerialize = function( tbl ) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function - - lookup_table = {} - - local function _Serialize( tbl ) - - if type(tbl) == 'table' then --function only works for tables! - - if lookup_table[tbl] then - return lookup_table[object] - end - - local tbl_str = {} - - lookup_table[tbl] = tbl_str - - tbl_str[#tbl_str + 1] = '{' - - for ind,val in pairs(tbl) do -- serialize its fields - local ind_str = {} - if type(ind) == "number" then - ind_str[#ind_str + 1] = '[' - ind_str[#ind_str + 1] = tostring(ind) - ind_str[#ind_str + 1] = ']=' - else --must be a string - ind_str[#ind_str + 1] = '[' - ind_str[#ind_str + 1] = routines.utils.basicSerialize(ind) - ind_str[#ind_str + 1] = ']=' - end - - local val_str = {} - if ((type(val) == 'number') or (type(val) == 'boolean')) then - val_str[#val_str + 1] = tostring(val) - val_str[#val_str + 1] = ',' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'string' then - val_str[#val_str + 1] = routines.utils.basicSerialize(val) - val_str[#val_str + 1] = ',' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'nil' then -- won't ever happen, right? - val_str[#val_str + 1] = 'nil,' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'table' then - if ind == "__index" then - -- tbl_str[#tbl_str + 1] = "__index" - -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it - else - - val_str[#val_str + 1] = _Serialize(val) - val_str[#val_str + 1] = ',' --I think this is right, I just added it - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - end - elseif type(val) == 'function' then - tbl_str[#tbl_str + 1] = "f() " .. tostring(ind) - tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it - else - env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)) - env.info( debug.traceback() ) - end - - end - tbl_str[#tbl_str + 1] = '}' - return table.concat(tbl_str) - else - return tostring(tbl) - end - end - - local objectreturn = _Serialize(tbl) - return objectreturn -end - ---porting in Slmod's "safestring" basic serialize -UTILS.BasicSerialize = function(s) - if s == nil then - return "\"\"" - else - if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'table') or (type(s) == 'userdata') ) then - return tostring(s) - elseif type(s) == 'string' then - s = string.format('%q', s) - return s - end - end -end - - -UTILS.ToDegree = function(angle) - return angle*180/math.pi -end - -UTILS.ToRadian = function(angle) - return angle*math.pi/180 -end - -UTILS.MetersToNM = function(meters) - return meters/1852 -end - -UTILS.MetersToFeet = function(meters) - return meters/0.3048 -end - -UTILS.NMToMeters = function(NM) - return NM*1852 -end - -UTILS.FeetToMeters = function(feet) - return feet*0.3048 -end - -UTILS.MpsToKnots = function(mps) - return mps*3600/1852 -end - -UTILS.MpsToKmph = function(mps) - return mps*3.6 -end - -UTILS.KnotsToMps = function(knots) - return knots*1852/3600 -end - -UTILS.KnotsToKmph = function(knots) - return knots* 1.852 -end - -UTILS.KmphToMps = function(kmph) - return kmph/3.6 -end - ---[[acc: -in DM: decimal point of minutes. -In DMS: decimal point of seconds. -position after the decimal of the least significant digit: -So: -42.32 - acc of 2. -]] -UTILS.tostringLL = function( lat, lon, acc, DMS) - - local latHemi, lonHemi - if lat > 0 then - latHemi = 'N' - else - latHemi = 'S' - end - - if lon > 0 then - lonHemi = 'E' - else - lonHemi = 'W' - end - - lat = math.abs(lat) - lon = math.abs(lon) - - local latDeg = math.floor(lat) - local latMin = (lat - latDeg)*60 - - local lonDeg = math.floor(lon) - local lonMin = (lon - lonDeg)*60 - - if DMS then -- degrees, minutes, and seconds. - local oldLatMin = latMin - latMin = math.floor(latMin) - local latSec = UTILS.Round((oldLatMin - latMin)*60, acc) - - local oldLonMin = lonMin - lonMin = math.floor(lonMin) - local lonSec = UTILS.Round((oldLonMin - lonMin)*60, acc) - - if latSec == 60 then - latSec = 0 - latMin = latMin + 1 - end - - if lonSec == 60 then - lonSec = 0 - lonMin = lonMin + 1 - end - - local secFrmtStr -- create the formatting string for the seconds place - secFrmtStr = '%02d' --- if acc <= 0 then -- no decimal place. --- secFrmtStr = '%02d' --- else --- local width = 3 + acc -- 01.310 - that's a width of 6, for example. --- secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' --- end - - return string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi - - else -- degrees, decimal minutes. - latMin = UTILS.Round(latMin, acc) - lonMin = UTILS.Round(lonMin, acc) - - if latMin == 60 then - latMin = 0 - latDeg = latDeg + 1 - end - - if lonMin == 60 then - lonMin = 0 - lonDeg = lonDeg + 1 - end - - local minFrmtStr -- create the formatting string for the minutes place - if acc <= 0 then -- no decimal place. - minFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end - - return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi - - end -end - --- acc- the accuracy of each easting/northing. 0, 1, 2, 3, 4, or 5. -UTILS.tostringMGRS = function(MGRS, acc) --R2.1 - if acc == 0 then - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph - else - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', UTILS.Round(MGRS.Easting/(10^(5-acc)), 0)) - .. ' ' .. string.format('%0' .. acc .. 'd', UTILS.Round(MGRS.Northing/(10^(5-acc)), 0)) - end -end - - ---- From http://lua-users.org/wiki/SimpleRound --- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place -function UTILS.Round( num, idp ) - local mult = 10 ^ ( idp or 0 ) - return math.floor( num * mult + 0.5 ) / mult -end - --- porting in Slmod's dostring -function UTILS.DoString( s ) - local f, err = loadstring( s ) - if f then - return true, f() - else - return false, err - end -end - --- Here is a customized version of pairs, which I called spairs because it iterates over the table in a sorted order. -function UTILS.spairs( t, order ) - -- collect the keys - local keys = {} - for k in pairs(t) do keys[#keys+1] = k end - - -- if order function given, sort by it by passing the table and keys a, b, - -- otherwise just sort the keys - if order then - table.sort(keys, function(a,b) return order(t, a, b) end) - else - table.sort(keys) - end - - -- return the iterator function - local i = 0 - return function() - i = i + 1 - if keys[i] then - return keys[i], t[keys[i]] - end - end -end - --- get a new mark ID for markings -function UTILS.GetMarkID() - - UTILS._MarkID = UTILS._MarkID + 1 - return UTILS._MarkID - -end - - --- Test if a Vec2 is in a radius of another Vec2 -function UTILS.IsInRadius( InVec2, Vec2, Radius ) - - local InRadius = ( ( InVec2.x - Vec2.x ) ^2 + ( InVec2.y - Vec2.y ) ^2 ) ^ 0.5 <= Radius - - return InRadius -end - --- Test if a Vec3 is in the sphere of another Vec3 -function UTILS.IsInSphere( InVec3, Vec3, Radius ) - - local InSphere = ( ( InVec3.x - Vec3.x ) ^2 + ( InVec3.y - Vec3.y ) ^2 + ( InVec3.z - Vec3.z ) ^2 ) ^ 0.5 <= Radius - - return InSphere -end ---- **Core** -- BASE forms **the basis of the MOOSE framework**. Each class within the MOOSE framework derives from BASE. --- --- ![Banner Image](..\Presentations\BASE\Dia1.JPG) --- --- === --- --- ### Author: **Sven Van de Velde (FlightControl)** --- ### Contributions: --- --- ==== --- --- @module Base - - - -local _TraceOnOff = true -local _TraceLevel = 1 -local _TraceAll = false -local _TraceClass = {} -local _TraceClassMethod = {} - -local _ClassID = 0 - ---- @type BASE --- @field ClassName The name of the class. --- @field ClassID The ID number of the class. --- @field ClassNameAndID The name of the class concatenated with the ID number of the class. - ---- # 1) #BASE class --- --- All classes within the MOOSE framework are derived from the BASE class. --- --- BASE provides facilities for : --- --- * The construction and inheritance of MOOSE classes. --- * The class naming and numbering system. --- * The class hierarchy search system. --- * The tracing of information or objects during mission execution for debuggin purposes. --- * The subscription to DCS events for event handling in MOOSE objects. --- --- Note: The BASE class is an abstract class and is not meant to be used directly. --- --- ## 1.1) BASE constructor --- --- Any class derived from BASE, will use the @{Base#BASE.New} constructor embedded in the @{Base#BASE.Inherit} method. --- See an example at the @{Base#BASE.New} method how this is done. --- --- ## 1.2) Trace information for debugging --- --- The BASE class contains trace methods to trace progress within a mission execution of a certain object. --- These trace methods are inherited by each MOOSE class interiting BASE, soeach object created from derived class from BASE can use the tracing methods to trace its execution. --- --- Any type of information can be passed to these tracing methods. See the following examples: --- --- self:E( "Hello" ) --- --- Result in the word "Hello" in the dcs.log. --- --- local Array = { 1, nil, "h", { "a","b" }, "x" } --- self:E( Array ) --- --- Results with the text [1]=1,[3]="h",[4]={[1]="a",[2]="b"},[5]="x"} in the dcs.log. --- --- local Object1 = "Object1" --- local Object2 = 3 --- local Object3 = { Object 1, Object 2 } --- self:E( { Object1, Object2, Object3 } ) --- --- Results with the text [1]={[1]="Object",[2]=3,[3]={[1]="Object",[2]=3}} in the dcs.log. --- --- local SpawnObject = SPAWN:New( "Plane" ) --- local GroupObject = GROUP:FindByName( "Group" ) --- self:E( { Spawn = SpawnObject, Group = GroupObject } ) --- --- Results with the text [1]={Spawn={....),Group={...}} in the dcs.log. --- --- Below a more detailed explanation of the different method types for tracing. --- --- ### 1.2.1) Tracing methods categories --- --- There are basically 3 types of tracing methods available: --- --- * @{#BASE.F}: Used to trace the entrance of a function and its given parameters. An F is indicated at column 44 in the DCS.log file. --- * @{#BASE.T}: Used to trace further logic within a function giving optional variables or parameters. A T is indicated at column 44 in the DCS.log file. --- * @{#BASE.E}: Used to always trace information giving optional variables or parameters. An E is indicated at column 44 in the DCS.log file. --- --- ### 1.2.2) Tracing levels --- --- There are 3 tracing levels within MOOSE. --- These tracing levels were defined to avoid bulks of tracing to be generated by lots of objects. --- --- As such, the F and T methods have additional variants to trace level 2 and 3 respectively: --- --- * @{#BASE.F2}: Trace the beginning of a function and its given parameters with tracing level 2. --- * @{#BASE.F3}: Trace the beginning of a function and its given parameters with tracing level 3. --- * @{#BASE.T2}: Trace further logic within a function giving optional variables or parameters with tracing level 2. --- * @{#BASE.T3}: Trace further logic within a function giving optional variables or parameters with tracing level 3. --- --- ### 1.2.3) Trace activation. --- --- Tracing can be activated in several ways: --- --- * Switch tracing on or off through the @{#BASE.TraceOnOff}() method. --- * Activate all tracing through the @{#BASE.TraceAll}() method. --- * Activate only the tracing of a certain class (name) through the @{#BASE.TraceClass}() method. --- * Activate only the tracing of a certain method of a certain class through the @{#BASE.TraceClassMethod}() method. --- * Activate only the tracing of a certain level through the @{#BASE.TraceLevel}() method. --- --- ### 1.2.4) Check if tracing is on. --- --- The method @{#BASE.IsTrace}() will validate if tracing is activated or not. --- --- ## 1.3 DCS simulator Event Handling --- --- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, --- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. --- --- ### 1.3.1 Subscribe / Unsubscribe to DCS Events --- --- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. --- So, when the DCS event occurs, the class will be notified of that event. --- There are two methods which you use to subscribe to or unsubscribe from an event. --- --- * @{#BASE.HandleEvent}(): Subscribe to a DCS Event. --- * @{#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. --- --- ### 1.3.2 Event Handling of DCS Events --- --- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called --- when the DCS event occurs. The Event Handling method receives an @{Event#EVENTDATA} structure, which contains a lot of information --- about the event that occurred. --- --- Find below an example of the prototype how to write an event handling function for two units: --- --- local Tank1 = UNIT:FindByName( "Tank A" ) --- local Tank2 = UNIT:FindByName( "Tank B" ) --- --- -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified. --- Tank1:HandleEvent( EVENTS.Dead ) --- Tank2:HandleEvent( EVENTS.Dead ) --- --- --- This function is an Event Handling function that will be called when Tank1 is Dead. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank1:OnEventDead( EventData ) --- --- self:SmokeGreen() --- end --- --- --- This function is an Event Handling function that will be called when Tank2 is Dead. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank2:OnEventDead( EventData ) --- --- self:SmokeBlue() --- end --- --- --- --- See the @{Event} module for more information about event handling. --- --- ## 1.4) Class identification methods --- --- BASE provides methods to get more information of each object: --- --- * @{#BASE.GetClassID}(): Gets the ID (number) of the object. Each object created is assigned a number, that is incremented by one. --- * @{#BASE.GetClassName}(): Gets the name of the object, which is the name of the class the object was instantiated from. --- * @{#BASE.GetClassNameAndID}(): Gets the name and ID of the object. --- --- ## 1.5) All objects derived from BASE can have "States" --- --- A mechanism is in place in MOOSE, that allows to let the objects administer **states**. --- States are essentially properties of objects, which are identified by a **Key** and a **Value**. --- --- The method @{#BASE.SetState}() can be used to set a Value with a reference Key to the object. --- To **read or retrieve** a state Value based on a Key, use the @{#BASE.GetState} method. --- --- These two methods provide a very handy way to keep state at long lasting processes. --- Values can be stored within the objects, and later retrieved or changed when needed. --- There is one other important thing to note, the @{#BASE.SetState}() and @{#BASE.GetState} methods --- receive as the **first parameter the object for which the state needs to be set**. --- Thus, if the state is to be set for the same object as the object for which the method is used, then provide the same --- object name to the method. --- --- ## 1.10) Inheritance --- --- The following methods are available to implement inheritance --- --- * @{#BASE.Inherit}: Inherits from a class. --- * @{#BASE.GetParent}: Returns the parent object from the object it is handling, or nil if there is no parent object. --- --- === --- --- @field #BASE BASE --- -BASE = { - ClassName = "BASE", - ClassID = 0, - Events = {}, - States = {}, -} - - ---- @field #BASE.__ -BASE.__ = {} - ---- @field #BASE._ -BASE._ = { - Schedules = {} --- Contains the Schedulers Active -} - ---- The Formation Class --- @type FORMATION --- @field Cone A cone formation. -FORMATION = { - Cone = "Cone", - Vee = "Vee" -} - - - ---- BASE constructor. --- --- This is an example how to use the BASE:New() constructor in a new class definition when inheriting from BASE. --- --- function EVENT:New() --- local self = BASE:Inherit( self, BASE:New() ) -- #EVENT --- return self --- end --- --- @param #BASE self --- @return #BASE -function BASE:New() - local self = routines.utils.deepCopy( self ) -- Create a new self instance - - _ClassID = _ClassID + 1 - self.ClassID = _ClassID - - -- This is for "private" methods... - -- When a __ is passed to a method as "self", the __index will search for the method on the public method list too! --- if rawget( self, "__" ) then - --setmetatable( self, { __index = self.__ } ) --- end - - return self -end - ---- This is the worker method to inherit from a parent class. --- @param #BASE self --- @param Child is the Child class that inherits. --- @param #BASE Parent is the Parent class that the Child inherits from. --- @return #BASE Child -function BASE:Inherit( Child, Parent ) - local Child = routines.utils.deepCopy( Child ) - - if Child ~= nil then - - -- This is for "private" methods... - -- When a __ is passed to a method as "self", the __index will search for the method on the public method list of the same object too! - if rawget( Child, "__" ) then - setmetatable( Child, { __index = Child.__ } ) - setmetatable( Child.__, { __index = Parent } ) - else - setmetatable( Child, { __index = Parent } ) - end - - --Child:_SetDestructor() - end - return Child -end - ---- This is the worker method to retrieve the Parent class. --- Note that the Parent class must be passed to call the parent class method. --- --- self:GetParent(self):ParentMethod() --- --- --- @param #BASE self --- @param #BASE Child is the Child class from which the Parent class needs to be retrieved. --- @return #BASE -function BASE:GetParent( Child ) - local Parent - -- BASE class has no parent - if Child.ClassName == 'BASE' then - Parent = nil - elseif rawget( Child, "__" ) then - Parent = getmetatable( Child.__ ).__index - else - Parent = getmetatable( Child ).__index - end - return Parent -end - ---- This is the worker method to check if an object is an (sub)instance of a class. --- --- ### Examples: --- --- * ZONE:New( 'some zone' ):IsInstanceOf( ZONE ) will return true --- * ZONE:New( 'some zone' ):IsInstanceOf( 'ZONE' ) will return true --- * ZONE:New( 'some zone' ):IsInstanceOf( 'zone' ) will return true --- * ZONE:New( 'some zone' ):IsInstanceOf( 'BASE' ) will return true --- --- * ZONE:New( 'some zone' ):IsInstanceOf( 'GROUP' ) will return false --- --- @param #BASE self --- @param ClassName is the name of the class or the class itself to run the check against --- @return #boolean -function BASE:IsInstanceOf( ClassName ) - - -- Is className NOT a string ? - if type( ClassName ) ~= 'string' then - - -- Is className a Moose class ? - if type( ClassName ) == 'table' and ClassName.ClassName ~= nil then - - -- Get the name of the Moose class as a string - ClassName = ClassName.ClassName - - -- className is neither a string nor a Moose class, throw an error - else - - -- I'm not sure if this should take advantage of MOOSE logging function, or throw an error for pcall - local err_str = 'className parameter should be a string; parameter received: '..type( ClassName ) - self:E( err_str ) - -- error( err_str ) - return false - - end - end - - ClassName = string.upper( ClassName ) - - if string.upper( self.ClassName ) == ClassName then - return true - end - - local Parent = self:GetParent(self) - - while Parent do - - if string.upper( Parent.ClassName ) == ClassName then - return true - end - - Parent = Parent:GetParent(Parent) - - end - - return false - -end ---- Get the ClassName + ClassID of the class instance. --- The ClassName + ClassID is formatted as '%s#%09d'. --- @param #BASE self --- @return #string The ClassName + ClassID of the class instance. -function BASE:GetClassNameAndID() - return string.format( '%s#%09d', self.ClassName, self.ClassID ) -end - ---- Get the ClassName of the class instance. --- @param #BASE self --- @return #string The ClassName of the class instance. -function BASE:GetClassName() - return self.ClassName -end - ---- Get the ClassID of the class instance. --- @param #BASE self --- @return #string The ClassID of the class instance. -function BASE:GetClassID() - return self.ClassID -end - -do -- Event Handling - - --- Returns the event dispatcher - -- @param #BASE self - -- @return Core.Event#EVENT - function BASE:EventDispatcher() - - return _EVENTDISPATCHER - end - - - --- Get the Class @{Event} processing Priority. - -- The Event processing Priority is a number from 1 to 10, - -- reflecting the order of the classes subscribed to the Event to be processed. - -- @param #BASE self - -- @return #number The @{Event} processing Priority. - function BASE:GetEventPriority() - return self._.EventPriority or 5 - end - - --- Set the Class @{Event} processing Priority. - -- The Event processing Priority is a number from 1 to 10, - -- reflecting the order of the classes subscribed to the Event to be processed. - -- @param #BASE self - -- @param #number EventPriority The @{Event} processing Priority. - -- @return self - function BASE:SetEventPriority( EventPriority ) - self._.EventPriority = EventPriority - end - - --- Remove all subscribed events - -- @param #BASE self - -- @return #BASE - function BASE:EventRemoveAll() - - self:EventDispatcher():RemoveAll( self ) - - return self - end - - --- Subscribe to a DCS Event. - -- @param #BASE self - -- @param Core.Event#EVENTS Event - -- @param #function EventFunction (optional) The function to be called when the event occurs for the unit. - -- @return #BASE - function BASE:HandleEvent( Event, EventFunction ) - - self:EventDispatcher():OnEventGeneric( EventFunction, self, Event ) - - return self - end - - --- UnSubscribe to a DCS event. - -- @param #BASE self - -- @param Core.Event#EVENTS Event - -- @return #BASE - function BASE:UnHandleEvent( Event ) - - self:EventDispatcher():RemoveEvent( self, Event ) - - return self - end - - -- Event handling function prototypes - - --- Occurs whenever any unit in a mission fires a weapon. But not any machine gun or autocannon based weapon, those are handled by EVENT.ShootingStart. - -- @function [parent=#BASE] OnEventShot - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs whenever an object is hit by a weapon. - -- initiator : The unit object the fired the weapon - -- weapon: Weapon object that hit the target - -- target: The Object that was hit. - -- @function [parent=#BASE] OnEventHit - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an aircraft takes off from an airbase, farp, or ship. - -- initiator : The unit that tookoff - -- place: Object from where the AI took-off from. Can be an Airbase Object, FARP, or Ships - -- @function [parent=#BASE] OnEventTakeoff - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an aircraft lands at an airbase, farp or ship - -- initiator : The unit that has landed - -- place: Object that the unit landed on. Can be an Airbase Object, FARP, or Ships - -- @function [parent=#BASE] OnEventLand - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any aircraft crashes into the ground and is completely destroyed. - -- initiator : The unit that has crashed - -- @function [parent=#BASE] OnEventCrash - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when a pilot ejects from an aircraft - -- initiator : The unit that has ejected - -- @function [parent=#BASE] OnEventEjection - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an aircraft connects with a tanker and begins taking on fuel. - -- initiator : The unit that is receiving fuel. - -- @function [parent=#BASE] OnEventRefueling - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an object is dead. - -- initiator : The unit that is dead. - -- @function [parent=#BASE] OnEventDead - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an object is completely destroyed. - -- initiator : The unit that is was destroyed. - -- @function [parent=#BASE] OnEvent - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when the pilot of an aircraft is killed. Can occur either if the player is alive and crashes or if a weapon kills the pilot without completely destroying the plane. - -- initiator : The unit that the pilot has died in. - -- @function [parent=#BASE] OnEventPilotDead - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when a ground unit captures either an airbase or a farp. - -- initiator : The unit that captured the base - -- place: The airbase that was captured, can be a FARP or Airbase. When calling place:getCoalition() the faction will already be the new owning faction. - -- @function [parent=#BASE] OnEventBaseCaptured - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when a mission starts - -- @function [parent=#BASE] OnEventMissionStart - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when a mission ends - -- @function [parent=#BASE] OnEventMissionEnd - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an aircraft is finished taking fuel. - -- initiator : The unit that was receiving fuel. - -- @function [parent=#BASE] OnEventRefuelingStop - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any object is spawned into the mission. - -- initiator : The unit that was spawned - -- @function [parent=#BASE] OnEventBirth - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any system fails on a human controlled aircraft. - -- initiator : The unit that had the failure - -- @function [parent=#BASE] OnEventHumanFailure - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any aircraft starts its engines. - -- initiator : The unit that is starting its engines. - -- @function [parent=#BASE] OnEventEngineStartup - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any aircraft shuts down its engines. - -- initiator : The unit that is stopping its engines. - -- @function [parent=#BASE] OnEventEngineShutdown - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any player assumes direct control of a unit. - -- initiator : The unit that is being taken control of. - -- @function [parent=#BASE] OnEventPlayerEnterUnit - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any player relieves control of a unit to the AI. - -- initiator : The unit that the player left. - -- @function [parent=#BASE] OnEventPlayerLeaveUnit - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any unit begins firing a weapon that has a high rate of fire. Most common with aircraft cannons (GAU-8), autocannons, and machine guns. - -- initiator : The unit that is doing the shooing. - -- target: The unit that is being targeted. - -- @function [parent=#BASE] OnEventShootingStart - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any unit stops firing its weapon. Event will always correspond with a shooting start event. - -- initiator : The unit that was doing the shooing. - -- @function [parent=#BASE] OnEventShootingEnd - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - -end - - ---- Creation of a Birth Event. --- @param #BASE self --- @param Dcs.DCSTypes#Time EventTime The time stamp of the event. --- @param Dcs.DCSWrapper.Object#Object Initiator The initiating object of the event. --- @param #string IniUnitName The initiating unit name. --- @param place --- @param subplace -function BASE:CreateEventBirth( EventTime, Initiator, IniUnitName, place, subplace ) - self:F( { EventTime, Initiator, IniUnitName, place, subplace } ) - - local Event = { - id = world.event.S_EVENT_BIRTH, - time = EventTime, - initiator = Initiator, - IniUnitName = IniUnitName, - place = place, - subplace = subplace - } - - world.onEvent( Event ) -end - ---- Creation of a Crash Event. --- @param #BASE self --- @param Dcs.DCSTypes#Time EventTime The time stamp of the event. --- @param Dcs.DCSWrapper.Object#Object Initiator The initiating object of the event. -function BASE:CreateEventCrash( EventTime, Initiator ) - self:F( { EventTime, Initiator } ) - - local Event = { - id = world.event.S_EVENT_CRASH, - time = EventTime, - initiator = Initiator, - } - - world.onEvent( Event ) -end - ---- Creation of a Takeoff Event. --- @param #BASE self --- @param Dcs.DCSTypes#Time EventTime The time stamp of the event. --- @param Dcs.DCSWrapper.Object#Object Initiator The initiating object of the event. -function BASE:CreateEventTakeoff( EventTime, Initiator ) - self:F( { EventTime, Initiator } ) - - local Event = { - id = world.event.S_EVENT_TAKEOFF, - time = EventTime, - initiator = Initiator, - } - - world.onEvent( Event ) -end - --- TODO: Complete Dcs.DCSTypes#Event structure. ---- The main event handling function... This function captures all events generated for the class. --- @param #BASE self --- @param Dcs.DCSTypes#Event event -function BASE:onEvent(event) - --self:F( { BaseEventCodes[event.id], event } ) - - if self then - for EventID, EventObject in pairs( self.Events ) do - if EventObject.EventEnabled then - --env.info( 'onEvent Table EventObject.Self = ' .. tostring(EventObject.Self) ) - --env.info( 'onEvent event.id = ' .. tostring(event.id) ) - --env.info( 'onEvent EventObject.Event = ' .. tostring(EventObject.Event) ) - if event.id == EventObject.Event then - if self == EventObject.Self then - if event.initiator and event.initiator:isExist() then - event.IniUnitName = event.initiator:getName() - end - if event.target and event.target:isExist() then - event.TgtUnitName = event.target:getName() - end - --self:T( { BaseEventCodes[event.id], event } ) - --EventObject.EventFunction( self, event ) - end - end - end - end - end -end - -do -- Scheduling - - --- Schedule a new time event. Note that the schedule will only take place if the scheduler is *started*. Even for a single schedule event, the scheduler needs to be started also. - -- @param #BASE self - -- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. - -- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. - -- @param #table ... Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. - -- @return #number The ScheduleID of the planned schedule. - function BASE:ScheduleOnce( Start, SchedulerFunction, ... ) - self:F2( { Start } ) - self:T3( { ... } ) - - local ObjectName = "-" - ObjectName = self.ClassName .. self.ClassID - - self:F3( { "ScheduleOnce: ", ObjectName, Start } ) - self.SchedulerObject = self - - local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( - self, - SchedulerFunction, - { ... }, - Start, - nil, - nil, - nil - ) - - self._.Schedules[#self.Schedules+1] = ScheduleID - - return self._.Schedules - end - - --- Schedule a new time event. Note that the schedule will only take place if the scheduler is *started*. Even for a single schedule event, the scheduler needs to be started also. - -- @param #BASE self - -- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. - -- @param #number Repeat Specifies the interval in seconds when the scheduler will call the event function. - -- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. - -- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. - -- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. - -- @param #table ... Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. - -- @return #number The ScheduleID of the planned schedule. - function BASE:ScheduleRepeat( Start, Repeat, RandomizeFactor, Stop, SchedulerFunction, ... ) - self:F2( { Start } ) - self:T3( { ... } ) - - local ObjectName = "-" - ObjectName = self.ClassName .. self.ClassID - - self:F3( { "ScheduleRepeat: ", ObjectName, Start, Repeat, RandomizeFactor, Stop } ) - self.SchedulerObject = self - - local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( - self, - SchedulerFunction, - { ... }, - Start, - Repeat, - RandomizeFactor, - Stop - ) - - self._.Schedules[SchedulerFunction] = ScheduleID - - return self._.Schedules - end - - --- Stops the Schedule. - -- @param #BASE self - -- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. - function BASE:ScheduleStop( SchedulerFunction ) - - self:F3( { "ScheduleStop:" } ) - - _SCHEDULEDISPATCHER:Stop( self, self._.Schedules[SchedulerFunction] ) - end - -end - - ---- Set a state or property of the Object given a Key and a Value. --- Note that if the Object is destroyed, nillified or garbage collected, then the Values and Keys will also be gone. --- @param #BASE self --- @param Object The object that will hold the Value set by the Key. --- @param Key The key that is used as a reference of the value. Note that the key can be a #string, but it can also be any other type! --- @param Value The value to is stored in the object. --- @return The Value set. --- @return #nil The Key was not found and thus the Value could not be retrieved. -function BASE:SetState( Object, Key, Value ) - - local ClassNameAndID = Object:GetClassNameAndID() - - self.States[ClassNameAndID] = self.States[ClassNameAndID] or {} - self.States[ClassNameAndID][Key] = Value - - return self.States[ClassNameAndID][Key] -end - - ---- Get a Value given a Key from the Object. --- Note that if the Object is destroyed, nillified or garbage collected, then the Values and Keys will also be gone. --- @param #BASE self --- @param Object The object that holds the Value set by the Key. --- @param Key The key that is used to retrieve the value. Note that the key can be a #string, but it can also be any other type! --- @return The Value retrieved. -function BASE:GetState( Object, Key ) - - local ClassNameAndID = Object:GetClassNameAndID() - - if self.States[ClassNameAndID] then - local Value = self.States[ClassNameAndID][Key] or false - return Value - end - - return nil -end - -function BASE:ClearState( Object, StateName ) - - local ClassNameAndID = Object:GetClassNameAndID() - if self.States[ClassNameAndID] then - self.States[ClassNameAndID][StateName] = nil - end -end - --- Trace section - --- Log a trace (only shown when trace is on) --- TODO: Make trace function using variable parameters. - ---- Set trace on or off --- Note that when trace is off, no debug statement is performed, increasing performance! --- When Moose is loaded statically, (as one file), tracing is switched off by default. --- So tracing must be switched on manually in your mission if you are using Moose statically. --- When moose is loading dynamically (for moose class development), tracing is switched on by default. --- @param #BASE self --- @param #boolean TraceOnOff Switch the tracing on or off. --- @usage --- -- Switch the tracing On --- BASE:TraceOnOff( true ) --- --- -- Switch the tracing Off --- BASE:TraceOnOff( false ) -function BASE:TraceOnOff( TraceOnOff ) - _TraceOnOff = TraceOnOff -end - - ---- Enquires if tracing is on (for the class). --- @param #BASE self --- @return #boolean -function BASE:IsTrace() - - if debug and ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then - return true - else - return false - end -end - ---- Set trace level --- @param #BASE self --- @param #number Level -function BASE:TraceLevel( Level ) - _TraceLevel = Level - self:E( "Tracing level " .. Level ) -end - ---- Trace all methods in MOOSE --- @param #BASE self --- @param #boolean TraceAll true = trace all methods in MOOSE. -function BASE:TraceAll( TraceAll ) - - _TraceAll = TraceAll - - if _TraceAll then - self:E( "Tracing all methods in MOOSE " ) - else - self:E( "Switched off tracing all methods in MOOSE" ) - end -end - ---- Set tracing for a class --- @param #BASE self --- @param #string Class -function BASE:TraceClass( Class ) - _TraceClass[Class] = true - _TraceClassMethod[Class] = {} - self:E( "Tracing class " .. Class ) -end - ---- Set tracing for a specific method of class --- @param #BASE self --- @param #string Class --- @param #string Method -function BASE:TraceClassMethod( Class, Method ) - if not _TraceClassMethod[Class] then - _TraceClassMethod[Class] = {} - _TraceClassMethod[Class].Method = {} - end - _TraceClassMethod[Class].Method[Method] = true - self:E( "Tracing method " .. Method .. " of class " .. Class ) -end - ---- Trace a function call. This function is private. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:_F( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) - - if debug and ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then - - local DebugInfoCurrent = DebugInfoCurrentParam and DebugInfoCurrentParam or debug.getinfo( 2, "nl" ) - local DebugInfoFrom = DebugInfoFromParam and DebugInfoFromParam or debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - if _TraceAll == true or _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then - local LineCurrent = 0 - if DebugInfoCurrent.currentline then - LineCurrent = DebugInfoCurrent.currentline - end - local LineFrom = 0 - if DebugInfoFrom then - LineFrom = DebugInfoFrom.currentline - end - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "F", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) - end - end -end - ---- Trace a function call. Must be at the beginning of the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:F( Arguments ) - - if debug and _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 1 then - self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - - ---- Trace a function call level 2. Must be at the beginning of the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:F2( Arguments ) - - if debug and _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 2 then - self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - ---- Trace a function call level 3. Must be at the beginning of the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:F3( Arguments ) - - if debug and _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 3 then - self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - ---- Trace a function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:_T( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) - - if debug and ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then - - local DebugInfoCurrent = DebugInfoCurrentParam and DebugInfoCurrentParam or debug.getinfo( 2, "nl" ) - local DebugInfoFrom = DebugInfoFromParam and DebugInfoFromParam or debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - if _TraceAll == true or _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then - local LineCurrent = 0 - if DebugInfoCurrent.currentline then - LineCurrent = DebugInfoCurrent.currentline - end - local LineFrom = 0 - if DebugInfoFrom then - LineFrom = DebugInfoFrom.currentline - end - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s" , LineCurrent, LineFrom, "T", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) - end - end -end - ---- Trace a function logic level 1. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:T( Arguments ) - - if debug and _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 1 then - self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - - ---- Trace a function logic level 2. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:T2( Arguments ) - - if debug and _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 2 then - self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - ---- Trace a function logic level 3. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:T3( Arguments ) - - if debug and _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 3 then - self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - ---- Log an exception which will be traced always. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:E( Arguments ) - - if debug then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - local LineCurrent = DebugInfoCurrent.currentline - local LineFrom = -1 - if DebugInfoFrom then - LineFrom = DebugInfoFrom.currentline - end - - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) - end - -end - - - ---- old stuff - ---function BASE:_Destructor() --- --self:E("_Destructor") --- --- --self:EventRemoveAll() ---end - - --- THIS IS WHY WE NEED LUA 5.2 ... ---function BASE:_SetDestructor() --- --- -- TODO: Okay, this is really technical... --- -- When you set a proxy to a table to catch __gc, weak tables don't behave like weak... --- -- Therefore, I am parking this logic until I've properly discussed all this with the community. --- --- local proxy = newproxy(true) --- local proxyMeta = getmetatable(proxy) --- --- proxyMeta.__gc = function () --- env.info("In __gc for " .. self:GetClassNameAndID() ) --- if self._Destructor then --- self:_Destructor() --- end --- end --- --- -- keep the userdata from newproxy reachable until the object --- -- table is about to be garbage-collected - then the __gc hook --- -- will be invoked and the destructor called --- rawset( self, '__proxy', proxy ) --- ---end--- The REPORT class --- @type REPORT --- @extends Core.Base#BASE -REPORT = { - ClassName = "REPORT", - Title = "", -} - ---- Create a new REPORT. --- @param #REPORT self --- @param #string Title --- @return #REPORT -function REPORT:New( Title ) - - local self = BASE:Inherit( self, BASE:New() ) -- #REPORT - - self.Report = {} - - self:SetTitle( Title or "" ) - self:SetIndent( 3 ) - - return self -end - ---- Has the REPORT Text? --- @param #REPORT self --- @return #boolean -function REPORT:HasText() --R2.1 - - return #self.Report > 0 -end - - ---- Set indent of a REPORT. --- @param #REPORT self --- @param #number Indent --- @return #REPORT -function REPORT:SetIndent( Indent ) --R2.1 - self.Indent = Indent - return self -end - - ---- Add a new line to a REPORT. --- @param #REPORT self --- @param #string Text --- @return #REPORT -function REPORT:Add( Text ) - self.Report[#self.Report+1] = Text - return self -end - ---- Add a new line to a REPORT. --- @param #REPORT self --- @param #string Text --- @return #REPORT -function REPORT:AddIndent( Text ) --R2.1 - self.Report[#self.Report+1] = string.rep(" ", self.Indent ) .. Text:gsub("\n","\n"..string.rep( " ", self.Indent ) ) - return self -end - ---- Produces the text of the report, taking into account an optional delimeter, which is \n by default. --- @param #REPORT self --- @param #string Delimiter (optional) A delimiter text. --- @return #string The report text. -function REPORT:Text( Delimiter ) - Delimiter = Delimiter or "\n" - local ReportText = ( self.Title ~= "" and self.Title .. Delimiter or self.Title ) .. table.concat( self.Report, Delimiter ) or "" - return ReportText -end - ---- Sets the title of the report. --- @param #REPORT self --- @param #string Title The title of the report. --- @return #REPORT -function REPORT:SetTitle( Title ) - self.Title = Title - return self -end - ---- Gets the amount of report items contained in the report. --- @param #REPORT self --- @return #number Returns the number of report items contained in the report. 0 is returned if no report items are contained in the report. The title is not counted for. -function REPORT:GetCount() - return #self.Report -end ---- **Core** -- SCHEDULER prepares and handles the **execution of functions over scheduled time (intervals)**. --- --- ![Banner Image](..\Presentations\SCHEDULER\Dia1.JPG) --- --- === --- --- SCHEDULER manages the **scheduling of functions**: --- --- * optionally in an optional specified time interval, --- * optionally **repeating** with a specified time repeat interval, --- * optionally **randomizing** with a specified time interval randomization factor, --- * optionally **stop** the repeating after a specified time interval. --- --- === --- --- # Demo Missions --- --- ### [SCHEDULER Demo Missions source code](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/SCH%20-%20Scheduler) --- --- ### [SCHEDULER Demo Missions, only for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SCH%20-%20Scheduler) --- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) --- --- ==== --- --- # YouTube Channel --- --- ### [SCHEDULER YouTube Channel (none)]() --- --- ==== --- --- ### Contributions: --- --- * FlightControl : Concept & Testing --- --- ### Authors: --- --- * FlightControl : Design & Programming --- --- === --- --- @module Scheduler - - ---- The SCHEDULER class --- @type SCHEDULER --- @field #number ScheduleID the ID of the scheduler. --- @extends Core.Base#BASE - - ---- # SCHEDULER class, extends @{Base#BASE} --- --- The SCHEDULER class creates schedule. --- --- A SCHEDULER can manage **multiple** (repeating) schedules. Each planned or executing schedule has a unique **ScheduleID**. --- The ScheduleID is returned when the method @{#SCHEDULER.Schedule}() is called. --- It is recommended to store the ScheduleID in a variable, as it is used in the methods @{SCHEDULER.Start}() and @{SCHEDULER.Stop}(), --- which can start and stop specific repeating schedules respectively within a SCHEDULER object. --- --- ## SCHEDULER constructor --- --- The SCHEDULER class is quite easy to use, but note that the New constructor has variable parameters: --- --- The @{#SCHEDULER.New}() method returns 2 variables: --- --- 1. The SCHEDULER object reference. --- 2. The first schedule planned in the SCHEDULER object. --- --- To clarify the different appliances, lets have a look at the following examples: --- --- ### Construct a SCHEDULER object without a persistent schedule. --- --- * @{#SCHEDULER.New}( nil ): Setup a new SCHEDULER object, which is persistently executed after garbage collection. --- --- SchedulerObject = SCHEDULER:New() --- SchedulerID = SchedulerObject:Schedule( nil, ScheduleFunction, {} ) --- --- The above example creates a new SchedulerObject, but does not schedule anything. --- A separate schedule is created by using the SchedulerObject using the method :Schedule..., which returns a ScheduleID --- --- ### Construct a SCHEDULER object without a volatile schedule, but volatile to the Object existence... --- --- * @{#SCHEDULER.New}( Object ): Setup a new SCHEDULER object, which is linked to the Object. When the Object is nillified or destroyed, the SCHEDULER object will also be destroyed and stopped after garbage collection. --- --- ZoneObject = ZONE:New( "ZoneName" ) --- SchedulerObject = SCHEDULER:New( ZoneObject ) --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {} ) --- ... --- ZoneObject = nil --- garbagecollect() --- --- The above example creates a new SchedulerObject, but does not schedule anything, and is bound to the existence of ZoneObject, which is a ZONE. --- A separate schedule is created by using the SchedulerObject using the method :Schedule()..., which returns a ScheduleID --- Later in the logic, the ZoneObject is put to nil, and garbage is collected. --- As a result, the ScheduleObject will cancel any planned schedule. --- --- ### Construct a SCHEDULER object with a persistent schedule. --- --- * @{#SCHEDULER.New}( nil, Function, FunctionArguments, Start, ... ): Setup a new persistent SCHEDULER object, and start a new schedule for the Function with the defined FunctionArguments according the Start and sequent parameters. --- --- SchedulerObject, SchedulerID = SCHEDULER:New( nil, ScheduleFunction, {} ) --- --- The above example creates a new SchedulerObject, and does schedule the first schedule as part of the call. --- Note that 2 variables are returned here: SchedulerObject, ScheduleID... --- --- ### Construct a SCHEDULER object without a schedule, but volatile to the Object existence... --- --- * @{#SCHEDULER.New}( Object, Function, FunctionArguments, Start, ... ): Setup a new SCHEDULER object, linked to Object, and start a new schedule for the Function with the defined FunctionArguments according the Start and sequent parameters. --- --- ZoneObject = ZONE:New( "ZoneName" ) --- SchedulerObject, SchedulerID = SCHEDULER:New( ZoneObject, ScheduleFunction, {} ) --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {} ) --- ... --- ZoneObject = nil --- garbagecollect() --- --- The above example creates a new SchedulerObject, and schedules a method call (ScheduleFunction), --- and is bound to the existence of ZoneObject, which is a ZONE object (ZoneObject). --- Both a ScheduleObject and a SchedulerID variable are returned. --- Later in the logic, the ZoneObject is put to nil, and garbage is collected. --- As a result, the ScheduleObject will cancel the planned schedule. --- --- ## SCHEDULER timer stopping and (re-)starting. --- --- The SCHEDULER can be stopped and restarted with the following methods: --- --- * @{#SCHEDULER.Start}(): (Re-)Start the schedules within the SCHEDULER object. If a CallID is provided to :Start(), only the schedule referenced by CallID will be (re-)started. --- * @{#SCHEDULER.Stop}(): Stop the schedules within the SCHEDULER object. If a CallID is provided to :Stop(), then only the schedule referenced by CallID will be stopped. --- --- ZoneObject = ZONE:New( "ZoneName" ) --- SchedulerObject, SchedulerID = SCHEDULER:New( ZoneObject, ScheduleFunction, {} ) --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 10 ) --- ... --- SchedulerObject:Stop( SchedulerID ) --- ... --- SchedulerObject:Start( SchedulerID ) --- --- The above example creates a new SchedulerObject, and does schedule the first schedule as part of the call. --- Note that 2 variables are returned here: SchedulerObject, ScheduleID... --- Later in the logic, the repeating schedule with SchedulerID is stopped. --- A bit later, the repeating schedule with SchedulerId is (re)-started. --- --- ## Create a new schedule --- --- With the method @{#SCHEDULER.Schedule}() a new time event can be scheduled. --- This method is used by the :New() constructor when a new schedule is planned. --- --- Consider the following code fragment of the SCHEDULER object creation. --- --- ZoneObject = ZONE:New( "ZoneName" ) --- SchedulerObject = SCHEDULER:New( ZoneObject ) --- --- Several parameters can be specified that influence the behaviour of a Schedule. --- --- ### A single schedule, immediately executed --- --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {} ) --- --- The above example schedules a new ScheduleFunction call to be executed asynchronously, within milleseconds ... --- --- ### A single schedule, planned over time --- --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {}, 10 ) --- --- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds ... --- --- ### A schedule with a repeating time interval, planned over time --- --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60 ) --- --- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds, --- and repeating 60 every seconds ... --- --- ### A schedule with a repeating time interval, planned over time, with time interval randomization --- --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60, 0.5 ) --- --- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds, --- and repeating 60 seconds, with a 50% time interval randomization ... --- So the repeating time interval will be randomized using the **0.5**, --- and will calculate between **60 - ( 60 * 0.5 )** and **60 + ( 60 * 0.5 )** for each repeat, --- which is in this example between **30** and **90** seconds. --- --- ### A schedule with a repeating time interval, planned over time, with time interval randomization, and stop after a time interval --- --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60, 0.5, 300 ) --- --- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds, --- The schedule will repeat every 60 seconds. --- So the repeating time interval will be randomized using the **0.5**, --- and will calculate between **60 - ( 60 * 0.5 )** and **60 + ( 60 * 0.5 )** for each repeat, --- which is in this example between **30** and **90** seconds. --- The schedule will stop after **300** seconds. --- --- @field #SCHEDULER -SCHEDULER = { - ClassName = "SCHEDULER", - Schedules = {}, -} - ---- SCHEDULER constructor. --- @param #SCHEDULER self --- @param #table SchedulerObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. --- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. --- @param #table SchedulerArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. --- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. --- @param #number Repeat Specifies the interval in seconds when the scheduler will call the event function. --- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. --- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. --- @return #SCHEDULER self. --- @return #number The ScheduleID of the planned schedule. -function SCHEDULER:New( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) - - local self = BASE:Inherit( self, BASE:New() ) -- #SCHEDULER - self:F2( { Start, Repeat, RandomizeFactor, Stop } ) - - local ScheduleID = nil - - self.MasterObject = SchedulerObject - - if SchedulerFunction then - ScheduleID = self:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) - end - - return self, ScheduleID -end - ---function SCHEDULER:_Destructor() --- --self:E("_Destructor") --- --- _SCHEDULEDISPATCHER:RemoveSchedule( self.CallID ) ---end - ---- Schedule a new time event. Note that the schedule will only take place if the scheduler is *started*. Even for a single schedule event, the scheduler needs to be started also. --- @param #SCHEDULER self --- @param #table SchedulerObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. --- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. --- @param #table SchedulerArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. --- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. --- @param #number Repeat Specifies the interval in seconds when the scheduler will call the event function. --- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. --- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. --- @return #number The ScheduleID of the planned schedule. -function SCHEDULER:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) - self:F2( { Start, Repeat, RandomizeFactor, Stop } ) - self:T3( { SchedulerArguments } ) - - local ObjectName = "-" - if SchedulerObject and SchedulerObject.ClassName and SchedulerObject.ClassID then - ObjectName = SchedulerObject.ClassName .. SchedulerObject.ClassID - end - self:F3( { "Schedule :", ObjectName, tostring( SchedulerObject ), Start, Repeat, RandomizeFactor, Stop } ) - self.SchedulerObject = SchedulerObject - - local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( - self, - SchedulerFunction, - SchedulerArguments, - Start, - Repeat, - RandomizeFactor, - Stop - ) - - self.Schedules[#self.Schedules+1] = ScheduleID - - return ScheduleID -end - ---- (Re-)Starts the schedules or a specific schedule if a valid ScheduleID is provided. --- @param #SCHEDULER self --- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. -function SCHEDULER:Start( ScheduleID ) - self:F3( { ScheduleID } ) - - _SCHEDULEDISPATCHER:Start( self, ScheduleID ) -end - ---- Stops the schedules or a specific schedule if a valid ScheduleID is provided. --- @param #SCHEDULER self --- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. -function SCHEDULER:Stop( ScheduleID ) - self:F3( { ScheduleID } ) - - _SCHEDULEDISPATCHER:Stop( self, ScheduleID ) -end - ---- Removes a specific schedule if a valid ScheduleID is provided. --- @param #SCHEDULER self --- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. -function SCHEDULER:Remove( ScheduleID ) - self:F3( { ScheduleID } ) - - _SCHEDULEDISPATCHER:Remove( self, ScheduleID ) -end - ---- Clears all pending schedules. --- @param #SCHEDULER self -function SCHEDULER:Clear() - self:F3( ) - - _SCHEDULEDISPATCHER:Clear( self ) -end - - - - - - - - - - - - - - ---- **Core** -- SCHEDULEDISPATCHER dispatches the different schedules. --- --- === --- --- Takes care of the creation and dispatching of scheduled functions for SCHEDULER objects. --- --- This class is tricky and needs some thorought explanation. --- SCHEDULE classes are used to schedule functions for objects, or as persistent objects. --- The SCHEDULEDISPATCHER class ensures that: --- --- - Scheduled functions are planned according the SCHEDULER object parameters. --- - Scheduled functions are repeated when requested, according the SCHEDULER object parameters. --- - Scheduled functions are automatically removed when the schedule is finished, according the SCHEDULER object parameters. --- --- The SCHEDULEDISPATCHER class will manage SCHEDULER object in memory during garbage collection: --- - When a SCHEDULER object is not attached to another object (that is, it's first :Schedule() parameter is nil), then the SCHEDULER --- object is _persistent_ within memory. --- - When a SCHEDULER object *is* attached to another object, then the SCHEDULER object is _not persistent_ within memory after a garbage collection! --- The none persistency of SCHEDULERS attached to objects is required to allow SCHEDULER objects to be garbage collectged, when the parent object is also desroyed or nillified and garbage collected. --- Even when there are pending timer scheduled functions to be executed for the SCHEDULER object, --- these will not be executed anymore when the SCHEDULER object has been destroyed. --- --- The SCHEDULEDISPATCHER allows multiple scheduled functions to be planned and executed for one SCHEDULER object. --- The SCHEDULER object therefore keeps a table of "CallID's", which are returned after each planning of a new scheduled function by the SCHEDULEDISPATCHER. --- The SCHEDULER object plans new scheduled functions through the @{Scheduler#SCHEDULER.Schedule}() method. --- The Schedule() method returns the CallID that is the reference ID for each planned schedule. --- --- === --- --- ### Contributions: - --- ### Authors: FlightControl : Design & Programming --- --- @module ScheduleDispatcher - ---- The SCHEDULEDISPATCHER structure --- @type SCHEDULEDISPATCHER -SCHEDULEDISPATCHER = { - ClassName = "SCHEDULEDISPATCHER", - CallID = 0, -} - -function SCHEDULEDISPATCHER:New() - local self = BASE:Inherit( self, BASE:New() ) - self:F3() - return self -end - ---- Add a Schedule to the ScheduleDispatcher. --- The development of this method was really tidy. --- It is constructed as such that a garbage collection is executed on the weak tables, when the Scheduler is nillified. --- Nothing of this code should be modified without testing it thoroughly. --- @param #SCHEDULEDISPATCHER self --- @param Core.Scheduler#SCHEDULER Scheduler -function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop ) - self:F2( { Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop } ) - - self.CallID = self.CallID + 1 - local CallID = self.CallID .. "#" .. ( Scheduler.MasterObject and Scheduler.MasterObject.GetClassNameAndID and Scheduler.MasterObject:GetClassNameAndID() or "" ) or "" - - -- Initialize the ObjectSchedulers array, which is a weakly coupled table. - -- If the object used as the key is nil, then the garbage collector will remove the item from the Functions array. - self.PersistentSchedulers = self.PersistentSchedulers or {} - - -- Initialize the ObjectSchedulers array, which is a weakly coupled table. - -- If the object used as the key is nil, then the garbage collector will remove the item from the Functions array. - self.ObjectSchedulers = self.ObjectSchedulers or setmetatable( {}, { __mode = "v" } ) - - if Scheduler.MasterObject then - self.ObjectSchedulers[CallID] = Scheduler - self:F3( { CallID = CallID, ObjectScheduler = tostring(self.ObjectSchedulers[CallID]), MasterObject = tostring(Scheduler.MasterObject) } ) - else - self.PersistentSchedulers[CallID] = Scheduler - self:F3( { CallID = CallID, PersistentScheduler = self.PersistentSchedulers[CallID] } ) - end - - self.Schedule = self.Schedule or setmetatable( {}, { __mode = "k" } ) - self.Schedule[Scheduler] = self.Schedule[Scheduler] or {} - self.Schedule[Scheduler][CallID] = {} - self.Schedule[Scheduler][CallID].Function = ScheduleFunction - self.Schedule[Scheduler][CallID].Arguments = ScheduleArguments - self.Schedule[Scheduler][CallID].StartTime = timer.getTime() + ( Start or 0 ) - self.Schedule[Scheduler][CallID].Start = Start + .1 - self.Schedule[Scheduler][CallID].Repeat = Repeat or 0 - self.Schedule[Scheduler][CallID].Randomize = Randomize or 0 - self.Schedule[Scheduler][CallID].Stop = Stop - - self:T3( self.Schedule[Scheduler][CallID] ) - - self.Schedule[Scheduler][CallID].CallHandler = function( CallID ) - self:F2( CallID ) - - local ErrorHandler = function( errmsg ) - env.info( "Error in timer function: " .. errmsg ) - if debug ~= nil then - env.info( debug.traceback() ) - end - return errmsg - end - - local Scheduler = self.ObjectSchedulers[CallID] - if not Scheduler then - Scheduler = self.PersistentSchedulers[CallID] - end - - --self:T3( { Scheduler = Scheduler } ) - - if Scheduler then - - local MasterObject = tostring(Scheduler.MasterObject) - local Schedule = self.Schedule[Scheduler][CallID] - - --self:T3( { Schedule = Schedule } ) - - local ScheduleObject = Scheduler.SchedulerObject - --local ScheduleObjectName = Scheduler.SchedulerObject:GetNameAndClassID() - local ScheduleFunction = Schedule.Function - local ScheduleArguments = Schedule.Arguments - local Start = Schedule.Start - local Repeat = Schedule.Repeat or 0 - local Randomize = Schedule.Randomize or 0 - local Stop = Schedule.Stop or 0 - local ScheduleID = Schedule.ScheduleID - - local Status, Result - if ScheduleObject then - local function Timer() - return ScheduleFunction( ScheduleObject, unpack( ScheduleArguments ) ) - end - Status, Result = xpcall( Timer, ErrorHandler ) - else - local function Timer() - return ScheduleFunction( unpack( ScheduleArguments ) ) - end - Status, Result = xpcall( Timer, ErrorHandler ) - end - - local CurrentTime = timer.getTime() - local StartTime = Schedule.StartTime - - self:F3( { Master = MasterObject, CurrentTime = CurrentTime, StartTime = StartTime, Start = Start, Repeat = Repeat, Randomize = Randomize, Stop = Stop } ) - - - if Status and (( Result == nil ) or ( Result and Result ~= false ) ) then - if Repeat ~= 0 and ( ( Stop == 0 ) or ( Stop ~= 0 and CurrentTime <= StartTime + Stop ) ) then - local ScheduleTime = - CurrentTime + - Repeat + - math.random( - - ( Randomize * Repeat / 2 ), - ( Randomize * Repeat / 2 ) - ) + - 0.01 - --self:T3( { Repeat = CallID, CurrentTime, ScheduleTime, ScheduleArguments } ) - return ScheduleTime -- returns the next time the function needs to be called. - else - self:Stop( Scheduler, CallID ) - end - else - self:Stop( Scheduler, CallID ) - end - else - self:E( "Scheduled obsolete call for CallID: " .. CallID ) - end - - return nil - end - - self:Start( Scheduler, CallID ) - - return CallID -end - -function SCHEDULEDISPATCHER:RemoveSchedule( Scheduler, CallID ) - self:F2( { Remove = CallID, Scheduler = Scheduler } ) - - if CallID then - self:Stop( Scheduler, CallID ) - self.Schedule[Scheduler][CallID] = nil - end -end - -function SCHEDULEDISPATCHER:Start( Scheduler, CallID ) - self:F2( { Start = CallID, Scheduler = Scheduler } ) - - if CallID then - local Schedule = self.Schedule[Scheduler] - -- Only start when there is no ScheduleID defined! - -- This prevents to "Start" the scheduler twice with the same CallID... - if not Schedule[CallID].ScheduleID then - Schedule[CallID].StartTime = timer.getTime() -- Set the StartTime field to indicate when the scheduler started. - Schedule[CallID].ScheduleID = timer.scheduleFunction( - Schedule[CallID].CallHandler, - CallID, - timer.getTime() + Schedule[CallID].Start - ) - end - else - for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do - self:Start( Scheduler, CallID ) -- Recursive - end - end -end - -function SCHEDULEDISPATCHER:Stop( Scheduler, CallID ) - self:F2( { Stop = CallID, Scheduler = Scheduler } ) - - if CallID then - local Schedule = self.Schedule[Scheduler] - -- Only stop when there is a ScheduleID defined for the CallID. - -- So, when the scheduler was stopped before, do nothing. - if Schedule[CallID].ScheduleID then - timer.removeFunction( Schedule[CallID].ScheduleID ) - Schedule[CallID].ScheduleID = nil - end - else - for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do - self:Stop( Scheduler, CallID ) -- Recursive - end - end -end - -function SCHEDULEDISPATCHER:Clear( Scheduler ) - self:F2( { Scheduler = Scheduler } ) - - for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do - self:Stop( Scheduler, CallID ) -- Recursive - end -end - - - ---- **Core** -- EVENT models DCS **event dispatching** using a **publish-subscribe** model. --- --- ![Banner Image](..\Presentations\EVENT\Dia1.JPG) --- --- === --- --- # 1) Event Handling Overview --- --- ![Objects](..\Presentations\EVENT\Dia2.JPG) --- --- Within a running mission, various DCS events occur. Units are dynamically created, crash, die, shoot stuff, get hit etc. --- This module provides a mechanism to dispatch those events occuring within your running mission, to the different objects orchestrating your mission. --- --- ![Objects](..\Presentations\EVENT\Dia3.JPG) --- --- Objects can subscribe to different events. The Event dispatcher will publish the received DCS events to the subscribed MOOSE objects, in a specified order. --- In this way, the subscribed MOOSE objects are kept in sync with your evolving running mission. --- --- ## 1.1) Event Dispatching --- --- ![Objects](..\Presentations\EVENT\Dia4.JPG) --- --- The _EVENTDISPATCHER object is automatically created within MOOSE, --- and handles the dispatching of DCS Events occurring --- in the simulator to the subscribed objects --- in the correct processing order. --- --- ![Objects](..\Presentations\EVENT\Dia5.JPG) --- --- There are 5 levels of kind of objects that the _EVENTDISPATCHER services: --- --- * _DATABASE object: The core of the MOOSE objects. Any object that is created, deleted or updated, is done in this database. --- * SET_ derived classes: Subsets of the _DATABASE object. These subsets are updated by the _EVENTDISPATCHER as the second priority. --- * UNIT objects: UNIT objects can subscribe to DCS events. Each DCS event will be directly published to teh subscribed UNIT object. --- * GROUP objects: GROUP objects can subscribe to DCS events. Each DCS event will be directly published to the subscribed GROUP object. --- * Any other object: Various other objects can subscribe to DCS events. Each DCS event triggered will be published to each subscribed object. --- --- ![Objects](..\Presentations\EVENT\Dia6.JPG) --- --- For most DCS events, the above order of updating will be followed. --- --- ![Objects](..\Presentations\EVENT\Dia7.JPG) --- --- But for some DCS events, the publishing order is reversed. This is due to the fact that objects need to be **erased** instead of added. --- --- ## 1.2) Event Handling --- --- ![Objects](..\Presentations\EVENT\Dia8.JPG) --- --- The actual event subscribing and handling is not facilitated through the _EVENTDISPATCHER, but it is done through the @{BASE} class, @{UNIT} class and @{GROUP} class. --- The _EVENTDISPATCHER is a component that is quietly working in the background of MOOSE. --- --- ![Objects](..\Presentations\EVENT\Dia9.JPG) --- --- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, --- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. --- --- ### 1.2.1 Subscribe / Unsubscribe to DCS Events --- --- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. --- So, when the DCS event occurs, the class will be notified of that event. --- There are two functions which you use to subscribe to or unsubscribe from an event. --- --- * @{Base#BASE.HandleEvent}(): Subscribe to a DCS Event. --- * @{Base#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. --- --- Note that for a UNIT, the event will be handled **for that UNIT only**! --- Note that for a GROUP, the event will be handled **for all the UNITs in that GROUP only**! --- --- For all objects of other classes, the subscribed events will be handled for **all UNITs within the Mission**! --- So if a UNIT within the mission has the subscribed event for that object, --- then the object event handler will receive the event for that UNIT! --- --- ### 1.3.2 Event Handling of DCS Events --- --- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called --- when the DCS event occurs. The Event Handling method receives an @{Event#EVENTDATA} structure, which contains a lot of information --- about the event that occurred. --- --- Find below an example of the prototype how to write an event handling function for two units: --- --- local Tank1 = UNIT:FindByName( "Tank A" ) --- local Tank2 = UNIT:FindByName( "Tank B" ) --- --- -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified. --- Tank1:HandleEvent( EVENTS.Dead ) --- Tank2:HandleEvent( EVENTS.Dead ) --- --- --- This function is an Event Handling function that will be called when Tank1 is Dead. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank1:OnEventDead( EventData ) --- --- self:SmokeGreen() --- end --- --- --- This function is an Event Handling function that will be called when Tank2 is Dead. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank2:OnEventDead( EventData ) --- --- self:SmokeBlue() --- end --- --- ### 1.3.3 Event Handling methods that are automatically called upon subscribed DCS events --- --- ![Objects](..\Presentations\EVENT\Dia10.JPG) --- --- The following list outlines which EVENTS item in the structure corresponds to which Event Handling method. --- Always ensure that your event handling methods align with the events being subscribed to, or nothing will be executed. --- --- # 2) EVENTS type --- --- The EVENTS structure contains names for all the different DCS events that objects can subscribe to using the --- @{Base#BASE.HandleEvent}() method. --- --- # 3) EVENTDATA type --- --- The @{Event#EVENTDATA} structure contains all the fields that are populated with event information before --- an Event Handler method is being called by the event dispatcher. --- The Event Handler received the EVENTDATA object as a parameter, and can be used to investigate further the different events. --- There are basically 4 main categories of information stored in the EVENTDATA structure: --- --- * Initiator Unit data: Several fields documenting the initiator unit related to the event. --- * Target Unit data: Several fields documenting the target unit related to the event. --- * Weapon data: Certain events populate weapon information. --- * Place data: Certain events populate place information. --- --- --- This function is an Event Handling function that will be called when Tank1 is Dead. --- -- EventData is an EVENTDATA structure. --- -- We use the EventData.IniUnit to smoke the tank Green. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank1:OnEventDead( EventData ) --- --- EventData.IniUnit:SmokeGreen() --- end --- --- --- Find below an overview which events populate which information categories: --- --- ![Objects](..\Presentations\EVENT\Dia14.JPG) --- --- **IMPORTANT NOTE:** Some events can involve not just UNIT objects, but also STATIC objects!!! --- In that case the initiator or target unit fields will refer to a STATIC object! --- In case a STATIC object is involved, the documentation indicates which fields will and won't not be populated. --- The fields **IniObjectCategory** and **TgtObjectCategory** contain the indicator which **kind of object is involved** in the event. --- You can use the enumerator **Object.Category.UNIT** and **Object.Category.STATIC** to check on IniObjectCategory and TgtObjectCategory. --- Example code snippet: --- --- if Event.IniObjectCategory == Object.Category.UNIT then --- ... --- end --- if Event.IniObjectCategory == Object.Category.STATIC then --- ... --- end --- --- When a static object is involved in the event, the Group and Player fields won't be populated. --- --- === --- --- ### Author: **Sven Van de Velde (FlightControl)** --- ### Contributions: --- --- ==== --- --- @module Event - - ---- The EVENT structure --- @type EVENT --- @field #EVENT.Events Events --- @extends Core.Base#BASE -EVENT = { - ClassName = "EVENT", - ClassID = 0, -} - -world.event.S_EVENT_NEW_CARGO = world.event.S_EVENT_MAX + 1000 -world.event.S_EVENT_DELETE_CARGO = world.event.S_EVENT_MAX + 1001 - ---- The different types of events supported by MOOSE. --- Use this structure to subscribe to events using the @{Base#BASE.HandleEvent}() method. --- @type EVENTS -EVENTS = { - Shot = world.event.S_EVENT_SHOT, - Hit = world.event.S_EVENT_HIT, - Takeoff = world.event.S_EVENT_TAKEOFF, - Land = world.event.S_EVENT_LAND, - Crash = world.event.S_EVENT_CRASH, - Ejection = world.event.S_EVENT_EJECTION, - Refueling = world.event.S_EVENT_REFUELING, - Dead = world.event.S_EVENT_DEAD, - PilotDead = world.event.S_EVENT_PILOT_DEAD, - BaseCaptured = world.event.S_EVENT_BASE_CAPTURED, - MissionStart = world.event.S_EVENT_MISSION_START, - MissionEnd = world.event.S_EVENT_MISSION_END, - TookControl = world.event.S_EVENT_TOOK_CONTROL, - RefuelingStop = world.event.S_EVENT_REFUELING_STOP, - Birth = world.event.S_EVENT_BIRTH, - HumanFailure = world.event.S_EVENT_HUMAN_FAILURE, - EngineStartup = world.event.S_EVENT_ENGINE_STARTUP, - EngineShutdown = world.event.S_EVENT_ENGINE_SHUTDOWN, - PlayerEnterUnit = world.event.S_EVENT_PLAYER_ENTER_UNIT, - PlayerLeaveUnit = world.event.S_EVENT_PLAYER_LEAVE_UNIT, - PlayerComment = world.event.S_EVENT_PLAYER_COMMENT, - ShootingStart = world.event.S_EVENT_SHOOTING_START, - ShootingEnd = world.event.S_EVENT_SHOOTING_END, - NewCargo = world.event.S_EVENT_NEW_CARGO, - DeleteCargo = world.event.S_EVENT_DELETE_CARGO, -} - ---- The Event structure --- Note that at the beginning of each field description, there is an indication which field will be populated depending on the object type involved in the Event: --- --- * A (Object.Category.)UNIT : A UNIT object type is involved in the Event. --- * A (Object.Category.)STATIC : A STATIC object type is involved in the Event.µ --- --- @type EVENTDATA --- @field #number id The identifier of the event. --- --- @field Dcs.DCSUnit#Unit initiator (UNIT/STATIC/SCENERY) The initiating @{Dcs.DCSUnit#Unit} or @{Dcs.DCSStaticObject#StaticObject}. --- @field Dcs.DCSObject#Object.Category IniObjectCategory (UNIT/STATIC/SCENERY) The initiator object category ( Object.Category.UNIT or Object.Category.STATIC ). --- @field Dcs.DCSUnit#Unit IniDCSUnit (UNIT/STATIC) The initiating @{DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. --- @field #string IniDCSUnitName (UNIT/STATIC) The initiating Unit name. --- @field Wrapper.Unit#UNIT IniUnit (UNIT/STATIC) The initiating MOOSE wrapper @{Unit#UNIT} of the initiator Unit object. --- @field #string IniUnitName (UNIT/STATIC) The initiating UNIT name (same as IniDCSUnitName). --- @field Dcs.DCSGroup#Group IniDCSGroup (UNIT) The initiating {DCSGroup#Group}. --- @field #string IniDCSGroupName (UNIT) The initiating Group name. --- @field Wrapper.Group#GROUP IniGroup (UNIT) The initiating MOOSE wrapper @{Group#GROUP} of the initiator Group object. --- @field #string IniGroupName UNIT) The initiating GROUP name (same as IniDCSGroupName). --- @field #string IniPlayerName (UNIT) The name of the initiating player in case the Unit is a client or player slot. --- @field Dcs.DCScoalition#coalition.side IniCoalition (UNIT) The coalition of the initiator. --- @field Dcs.DCSUnit#Unit.Category IniCategory (UNIT) The category of the initiator. --- @field #string IniTypeName (UNIT) The type name of the initiator. --- --- @field Dcs.DCSUnit#Unit target (UNIT/STATIC) The target @{Dcs.DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. --- @field Dcs.DCSObject#Object.Category TgtObjectCategory (UNIT/STATIC) The target object category ( Object.Category.UNIT or Object.Category.STATIC ). --- @field Dcs.DCSUnit#Unit TgtDCSUnit (UNIT/STATIC) The target @{DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. --- @field #string TgtDCSUnitName (UNIT/STATIC) The target Unit name. --- @field Wrapper.Unit#UNIT TgtUnit (UNIT/STATIC) The target MOOSE wrapper @{Unit#UNIT} of the target Unit object. --- @field #string TgtUnitName (UNIT/STATIC) The target UNIT name (same as TgtDCSUnitName). --- @field Dcs.DCSGroup#Group TgtDCSGroup (UNIT) The target {DCSGroup#Group}. --- @field #string TgtDCSGroupName (UNIT) The target Group name. --- @field Wrapper.Group#GROUP TgtGroup (UNIT) The target MOOSE wrapper @{Group#GROUP} of the target Group object. --- @field #string TgtGroupName (UNIT) The target GROUP name (same as TgtDCSGroupName). --- @field #string TgtPlayerName (UNIT) The name of the target player in case the Unit is a client or player slot. --- @field Dcs.DCScoalition#coalition.side TgtCoalition (UNIT) The coalition of the target. --- @field Dcs.DCSUnit#Unit.Category TgtCategory (UNIT) The category of the target. --- @field #string TgtTypeName (UNIT) The type name of the target. --- --- @field weapon The weapon used during the event. --- @field Weapon --- @field WeaponName --- @field WeaponTgtDCSUnit - - - -local _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" - }, - [EVENTS.NewCargo] = { - Order = 1, - Event = "OnEventNewCargo", - Text = "S_EVENT_NEW_CARGO" - }, - [EVENTS.DeleteCargo] = { - Order = 1, - Event = "OnEventDeleteCargo", - Text = "S_EVENT_DELETE_CARGO" - }, -} - - ---- The Events structure --- @type EVENT.Events --- @field #number IniUnit - -function EVENT:New() - local self = BASE:Inherit( self, BASE:New() ) - self:F2() - self.EventHandler = world.addEventHandler( self ) - return self -end - - ---- Initializes the Events structure for the event --- @param #EVENT self --- @param Dcs.DCSWorld#world.event EventID --- @param Core.Base#BASE EventClass --- @return #EVENT.Events -function EVENT:Init( EventID, EventClass ) - self:F3( { _EVENTMETA[EventID].Text, EventClass } ) - - if not self.Events[EventID] then - -- Create a WEAK table to ensure that the garbage collector is cleaning the event links when the object usage is cleaned. - self.Events[EventID] = {} - end - - -- Each event has a subtable of EventClasses, ordered by EventPriority. - local EventPriority = EventClass:GetEventPriority() - if not self.Events[EventID][EventPriority] then - self.Events[EventID][EventPriority] = setmetatable( {}, { __mode = "k" } ) - end - - if not self.Events[EventID][EventPriority][EventClass] then - self.Events[EventID][EventPriority][EventClass] = {} - end - return self.Events[EventID][EventPriority][EventClass] -end - ---- Removes a subscription --- @param #EVENT self --- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param Dcs.DCSWorld#world.event EventID --- @return #EVENT.Events -function EVENT:RemoveEvent( EventClass, EventID ) - - self:F2( { "Removing subscription for class: ", EventClass:GetClassNameAndID() } ) - - local EventPriority = EventClass:GetEventPriority() - - self.Events = self.Events or {} - self.Events[EventID] = self.Events[EventID] or {} - self.Events[EventID][EventPriority] = self.Events[EventID][EventPriority] or {} - self.Events[EventID][EventPriority][EventClass] = self.Events[EventID][EventPriority][EventClass] - - self.Events[EventID][EventPriority][EventClass] = nil - -end - ---- Resets subscriptions --- @param #EVENT self --- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param Dcs.DCSWorld#world.event EventID --- @return #EVENT.Events -function EVENT:Reset( EventObject ) --R2.1 - - self:E( { "Resetting subscriptions for class: ", EventObject:GetClassNameAndID() } ) - - local EventPriority = EventObject:GetEventPriority() - for EventID, EventData in pairs( self.Events ) do - if self.EventsDead then - if self.EventsDead[EventID] then - if self.EventsDead[EventID][EventPriority] then - if self.EventsDead[EventID][EventPriority][EventObject] then - self.Events[EventID][EventPriority][EventObject] = self.EventsDead[EventID][EventPriority][EventObject] - end - end - end - end - end -end - - - - ---- Clears all event subscriptions for a @{Base#BASE} derived object. --- @param #EVENT self --- @param Core.Base#BASE EventObject -function EVENT:RemoveAll( EventObject ) - self:F3( { EventObject:GetClassNameAndID() } ) - - local EventClass = EventObject:GetClassNameAndID() - local EventPriority = EventClass:GetEventPriority() - for EventID, EventData in pairs( self.Events ) do - self.Events[EventID][EventPriority][EventClass] = nil - end -end - - - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventClass The instance of the class for which the event is. --- @param #function OnEventFunction --- @return #EVENT -function EVENT:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EventID ) - self:F2( EventTemplate.name ) - - for EventUnitID, EventUnit in pairs( EventTemplate.units ) do - self:OnEventForUnit( EventUnit.name, EventFunction, EventClass, EventID ) - end - return self -end - ---- Set a new listener for an S_EVENT_X event independent from a unit or a weapon. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Core.Base#BASE EventClass The self instance of the class for which the event is captured. When the event happens, the event process will be called in this class provided. --- @param EventID --- @return #EVENT -function EVENT:OnEventGeneric( EventFunction, EventClass, EventID ) - self:F2( { EventID } ) - - local EventData = self:Init( EventID, EventClass ) - EventData.EventFunction = EventFunction - - return self -end - - ---- Set a new listener for an S_EVENT_X event for a UNIT. --- @param #EVENT self --- @param #string UnitName The name of the UNIT. --- @param #function EventFunction The function to be called when the event occurs for the GROUP. --- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param EventID --- @return #EVENT -function EVENT:OnEventForUnit( UnitName, EventFunction, EventClass, EventID ) - self:F2( UnitName ) - - local EventData = self:Init( EventID, EventClass ) - EventData.EventUnit = true - EventData.EventFunction = EventFunction - return self -end - ---- Set a new listener for an S_EVENT_X event for a GROUP. --- @param #EVENT self --- @param #string GroupName The name of the GROUP. --- @param #function EventFunction The function to be called when the event occurs for the GROUP. --- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param EventID --- @return #EVENT -function EVENT:OnEventForGroup( GroupName, EventFunction, EventClass, EventID, ... ) - self:E( GroupName ) - - local Event = self:Init( EventID, EventClass ) - Event.EventGroup = true - Event.EventFunction = EventFunction - Event.Params = arg - return self -end - -do -- OnBirth - - --- Create an OnBirth event handler for a group - -- @param #EVENT self - -- @param Wrapper.Group#GROUP EventGroup - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnBirthForTemplate( EventTemplate, EventFunction, EventClass ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Birth ) - - return self - end - -end - -do -- OnCrash - - --- Create an OnCrash event handler for a group - -- @param #EVENT self - -- @param Wrapper.Group#GROUP EventGroup - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnCrashForTemplate( EventTemplate, EventFunction, EventClass ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Crash ) - - return self - end - -end - -do -- OnDead - - --- Create an OnDead event handler for a group - -- @param #EVENT self - -- @param Wrapper.Group#GROUP EventGroup - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnDeadForTemplate( EventTemplate, EventFunction, EventClass ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Dead ) - - return self - end - -end - - -do -- OnLand - --- Create an OnLand event handler for a group - -- @param #EVENT self - -- @param #table EventTemplate - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnLandForTemplate( EventTemplate, EventFunction, EventClass ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Land ) - - return self - end - -end - -do -- OnTakeOff - --- Create an OnTakeOff event handler for a group - -- @param #EVENT self - -- @param #table EventTemplate - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnTakeOffForTemplate( EventTemplate, EventFunction, EventClass ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Takeoff ) - - return self - end - -end - -do -- OnEngineShutDown - - --- Create an OnDead event handler for a group - -- @param #EVENT self - -- @param #table EventTemplate - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnEngineShutDownForTemplate( EventTemplate, EventFunction, EventClass ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.EngineShutdown ) - - return self - end - -end - -do -- Event Creation - - --- Creation of a New Cargo Event. - -- @param #EVENT self - -- @param AI.AI_Cargo#AI_CARGO Cargo The Cargo created. - function EVENT:CreateEventNewCargo( Cargo ) - self:F( { Cargo } ) - - local Event = { - id = EVENTS.NewCargo, - time = timer.getTime(), - cargo = Cargo, - } - - world.onEvent( Event ) - end - - --- Creation of a Cargo Deletion Event. - -- @param #EVENT self - -- @param AI.AI_Cargo#AI_CARGO Cargo The Cargo created. - function EVENT:CreateEventDeleteCargo( Cargo ) - self:F( { Cargo } ) - - local Event = { - id = EVENTS.DeleteCargo, - time = timer.getTime(), - cargo = Cargo, - } - - world.onEvent( Event ) - end - - --- Creation of a S_EVENT_PLAYER_ENTER_UNIT Event. - -- @param #EVENT self - -- @param Wrapper.Unit#UNIT PlayerUnit. - function EVENT:CreateEventPlayerEnterUnit( PlayerUnit ) - self:F( { PlayerUnit } ) - - local Event = { - id = EVENTS.PlayerEnterUnit, - time = timer.getTime(), - initiator = PlayerUnit:GetDCSObject() - } - - world.onEvent( Event ) - end - -end - ---- @param #EVENT self --- @param #EVENTDATA Event -function EVENT:onEvent( Event ) - - local ErrorHandler = function( errmsg ) - - env.info( "Error in SCHEDULER function:" .. errmsg ) - if debug ~= nil then - env.info( debug.traceback() ) - end - - return errmsg - end - - - local EventMeta = _EVENTMETA[Event.id] - - if self and - self.Events and - self.Events[Event.id] and - ( Event.initiator ~= nil or ( Event.initiator == nil and Event.id ~= EVENTS.PlayerLeaveUnit ) ) then - - if Event.initiator then - - Event.IniObjectCategory = Event.initiator:getCategory() - - if Event.IniObjectCategory == Object.Category.UNIT then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniDCSGroup = Event.IniDCSUnit:getGroup() - Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName ) - if not Event.IniUnit then - -- Unit can be a CLIENT. Most likely this will be the case ... - Event.IniUnit = CLIENT:FindByName( Event.IniDCSUnitName, '', true ) - end - Event.IniDCSGroupName = "" - if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then - Event.IniDCSGroupName = Event.IniDCSGroup:getName() - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - if Event.IniGroup then - Event.IniGroupName = Event.IniDCSGroupName - end - end - Event.IniPlayerName = Event.IniDCSUnit:getPlayerName() - Event.IniCoalition = Event.IniDCSUnit:getCoalition() - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - Event.IniCategory = Event.IniDCSUnit:getDesc().category - end - - if Event.IniObjectCategory == Object.Category.STATIC then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = STATIC:FindByName( Event.IniDCSUnitName, false ) - Event.IniCoalition = Event.IniDCSUnit:getCoalition() - Event.IniCategory = Event.IniDCSUnit:getDesc().category - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - end - - if Event.IniObjectCategory == Object.Category.SCENERY then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = SCENERY:Register( Event.IniDCSUnitName, Event.initiator ) - Event.IniCategory = Event.IniDCSUnit:getDesc().category - Event.IniTypeName = Event.initiator:isExist() and Event.IniDCSUnit:getTypeName() or "SCENERY" -- TODO: Bug fix for 2.1! - end - end - - if Event.target then - - Event.TgtObjectCategory = Event.target:getCategory() - - if Event.TgtObjectCategory == Object.Category.UNIT then - Event.TgtDCSUnit = Event.target - Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = UNIT:FindByName( Event.TgtDCSUnitName ) - Event.TgtDCSGroupName = "" - if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then - Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() - Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) - if Event.TgtGroup then - Event.TgtGroupName = Event.TgtDCSGroupName - end - end - Event.TgtPlayerName = Event.TgtDCSUnit:getPlayerName() - Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - - if Event.TgtObjectCategory == Object.Category.STATIC then - Event.TgtDCSUnit = Event.target - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName ) - Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - - if Event.TgtObjectCategory == Object.Category.SCENERY then - Event.TgtDCSUnit = Event.target - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = SCENERY:Register( Event.TgtDCSUnitName, Event.target ) - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - end - - if Event.weapon then - Event.Weapon = Event.weapon - Event.WeaponName = Event.Weapon:getTypeName() - Event.WeaponUNIT = CLIENT:Find( Event.Weapon, '', true ) -- Sometimes, the weapon is a player unit! - Event.WeaponPlayerName = Event.WeaponUNIT and Event.Weapon:getPlayerName() - Event.WeaponCoalition = Event.WeaponUNIT and Event.Weapon:getCoalition() - Event.WeaponCategory = Event.WeaponUNIT and Event.Weapon:getDesc().category - Event.WeaponTypeName = Event.WeaponUNIT and Event.Weapon:getTypeName() - --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() - end - - if Event.cargo then - Event.Cargo = Event.cargo - Event.CargoName = Event.cargo.Name - end - - local PriorityOrder = EventMeta.Order - local PriorityBegin = PriorityOrder == -1 and 5 or 1 - local PriorityEnd = PriorityOrder == -1 and 1 or 5 - - if Event.IniObjectCategory ~= Object.Category.STATIC then - self:E( { EventMeta.Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) - end - - for EventPriority = PriorityBegin, PriorityEnd, PriorityOrder do - - if self.Events[Event.id][EventPriority] then - - -- Okay, we got the event from DCS. Now loop the SORTED self.EventSorted[] table for the received Event.id, and for each EventData registered, check if a function needs to be called. - for EventClass, EventData in pairs( self.Events[Event.id][EventPriority] ) do - - --if Event.IniObjectCategory ~= Object.Category.STATIC then - -- self:E( { "Evaluating: ", EventClass:GetClassNameAndID() } ) - --end - - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) - - -- If the EventData is for a UNIT, the call directly the EventClass EventFunction for that UNIT. - if EventData.EventUnit then - - -- So now the EventClass must be a UNIT class!!! We check if it is still "Alive". - if EventClass:IsAlive() or - Event.id == EVENTS.Crash or - Event.id == EVENTS.Dead then - - local UnitName = EventClass:GetName() - - if ( EventMeta.Side == "I" and UnitName == Event.IniDCSUnitName ) or - ( EventMeta.Side == "T" and UnitName == Event.TgtDCSUnitName ) then - - -- First test if a EventFunction is Set, otherwise search for the default function - if EventData.EventFunction then - - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling EventFunction for UNIT ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventData.EventFunction( EventClass, Event ) - end, ErrorHandler ) - - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ EventMeta.Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling " .. EventMeta.Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventFunction( EventClass, Event ) - end, ErrorHandler ) - end - end - end - else - -- The EventClass is not alive anymore, we remove it from the EventHandlers... - self:RemoveEvent( EventClass, Event.id ) - end - else - - -- If the EventData is for a GROUP, the call directly the EventClass EventFunction for the UNIT in that GROUP. - if EventData.EventGroup then - - -- So now the EventClass must be a GROUP class!!! We check if it is still "Alive". - if EventClass:IsAlive() or - Event.id == EVENTS.Crash or - Event.id == EVENTS.Dead then - - -- We can get the name of the EventClass, which is now always a GROUP object. - local GroupName = EventClass:GetName() - - if ( EventMeta.Side == "I" and GroupName == Event.IniDCSGroupName ) or - ( EventMeta.Side == "T" and GroupName == Event.TgtDCSGroupName ) then - - -- First test if a EventFunction is Set, otherwise search for the default function - if EventData.EventFunction then - - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling EventFunction for GROUP ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventData.EventFunction( EventClass, Event, unpack( EventData.Params ) ) - end, ErrorHandler ) - - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ EventMeta.Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling " .. EventMeta.Event .. " for GROUP ", EventClass:GetClassNameAndID(), EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventFunction( EventClass, Event, unpack( EventData.Params ) ) - end, ErrorHandler ) - end - end - end - else - -- The EventClass is not alive anymore, we remove it from the EventHandlers... - --self:RemoveEvent( EventClass, Event.id ) - end - else - - -- If the EventData is not bound to a specific unit, then call the EventClass EventFunction. - -- Note that here the EventFunction will need to implement and determine the logic for the relevant source- or target unit, or weapon. - if not EventData.EventUnit then - - -- First test if a EventFunction is Set, otherwise search for the default function - if EventData.EventFunction then - - -- There is an EventFunction defined, so call the EventFunction. - if Event.IniObjectCategory ~= 3 then - self:F2( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - end - local Result, Value = xpcall( - function() - return EventData.EventFunction( EventClass, Event ) - end, ErrorHandler ) - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ EventMeta.Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - if Event.IniObjectCategory ~= 3 then - self:F2( { "Calling " .. EventMeta.Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - end - - local Result, Value = xpcall( - function() - local Result, Value = EventFunction( EventClass, Event ) - return Result, Value - end, ErrorHandler ) - end - end - - end - end - end - end - end - end - else - self:E( { EventMeta.Text, Event } ) - end - - Event = nil -end - ---- The EVENTHANDLER structure --- @type EVENTHANDLER --- @extends Core.Base#BASE -EVENTHANDLER = { - ClassName = "EVENTHANDLER", - ClassID = 0, -} - ---- The EVENTHANDLER constructor --- @param #EVENTHANDLER self --- @return #EVENTHANDLER -function EVENTHANDLER:New() - self = BASE:Inherit( self, BASE:New() ) -- #EVENTHANDLER - return self -end ---- **Core** -- **SETTINGS** classe defines the format settings management for measurement. --- --- ![Banner Image](..\Presentations\SETTINGS\Dia1.JPG) --- --- ==== --- --- # Demo Missions --- --- ### [SETTINGS Demo Missions source code]() --- --- ### [SETTINGS Demo Missions, only for beta testers]() --- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) --- --- ==== --- --- # YouTube Channel --- --- ### [SETTINGS YouTube Channel]() --- --- === --- --- ### Author: **Sven Van de Velde (FlightControl)** --- ### Contributions: --- --- ==== --- --- @module Settings - - ---- @type SETTINGS --- @field #number LL_Accuracy --- @field #boolean LL_DMS --- @field #number MGRS_Accuracy --- @field #string A2GSystem --- @field #string A2ASystem --- @extends Core.Base#BASE - ---- # SETTINGS class, extends @{Base#BASE} --- --- @field #SETTINGS -SETTINGS = { - ClassName = "SETTINGS", -} - - - -do -- SETTINGS - - --- SETTINGS constructor. - -- @param #SETTINGS self - -- @return #SETTINGS - function SETTINGS:Set( PlayerName ) - - if PlayerName == nil then - local self = BASE:Inherit( self, BASE:New() ) -- #SETTINGS - self:SetMetric() -- Defaults - self:SetA2G_BR() -- Defaults - self:SetA2A_BRAA() -- Defaults - self:SetLL_Accuracy( 3 ) -- Defaults - self:SetMGRS_Accuracy( 5 ) -- Defaults - self:SetMessageTime( MESSAGE.Type.Briefing, 180 ) - self:SetMessageTime( MESSAGE.Type.Detailed, 60 ) - self:SetMessageTime( MESSAGE.Type.Information, 30 ) - self:SetMessageTime( MESSAGE.Type.Overview, 60 ) - self:SetMessageTime( MESSAGE.Type.Update, 15 ) - return self - else - local Settings = _DATABASE:GetPlayerSettings( PlayerName ) - if not Settings then - Settings = BASE:Inherit( self, BASE:New() ) -- #SETTINGS - _DATABASE:SetPlayerSettings( PlayerName, Settings ) - end - return Settings - end - end - - - --- Sets the SETTINGS metric. - -- @param #SETTINGS self - function SETTINGS:SetMetric() - self.Metric = true - end - - --- Gets if the SETTINGS is metric. - -- @param #SETTINGS self - -- @return #boolean true if metric. - function SETTINGS:IsMetric() - return ( self.Metric ~= nil and self.Metric == true ) or ( self.Metric == nil and _SETTINGS:IsMetric() ) - end - - --- Sets the SETTINGS imperial. - -- @param #SETTINGS self - function SETTINGS:SetImperial() - self.Metric = false - end - - --- Gets if the SETTINGS is imperial. - -- @param #SETTINGS self - -- @return #boolean true if imperial. - function SETTINGS:IsImperial() - return ( self.Metric ~= nil and self.Metric == false ) or ( self.Metric == nil and _SETTINGS:IsMetric() ) - end - - --- Sets the SETTINGS LL accuracy. - -- @param #SETTINGS self - -- @param #number LL_Accuracy - -- @return #SETTINGS - function SETTINGS:SetLL_Accuracy( LL_Accuracy ) - self.LL_Accuracy = LL_Accuracy - end - - --- Gets the SETTINGS LL accuracy. - -- @param #SETTINGS self - -- @return #number - function SETTINGS:GetLL_DDM_Accuracy() - return self.LL_DDM_Accuracy or _SETTINGS:GetLL_DDM_Accuracy() - end - - --- Sets the SETTINGS MGRS accuracy. - -- @param #SETTINGS self - -- @param #number MGRS_Accuracy - -- @return #SETTINGS - function SETTINGS:SetMGRS_Accuracy( MGRS_Accuracy ) - self.MGRS_Accuracy = MGRS_Accuracy - end - - --- Gets the SETTINGS MGRS accuracy. - -- @param #SETTINGS self - -- @return #number - function SETTINGS:GetMGRS_Accuracy() - return self.MGRS_Accuracy or _SETTINGS:GetMGRS_Accuracy() - end - - --- Sets the SETTINGS Message Display Timing of a MessageType - -- @param #SETTINGS self - -- @param Core.Message#MESSAGE MessageType The type of the message. - -- @param #number MessageTime The display time duration in seconds of the MessageType. - function SETTINGS:SetMessageTime( MessageType, MessageTime ) - self.MessageTypeTimings = self.MessageTypeTimings or {} - self.MessageTypeTimings[MessageType] = MessageTime - end - - - --- Gets the SETTINGS Message Display Timing of a MessageType - -- @param #SETTINGS self - -- @param Core.Message#MESSAGE MessageType The type of the message. - -- @return #number - function SETTINGS:GetMessageTime( MessageType ) - return ( self.MessageTypeTimings and self.MessageTypeTimings[MessageType] ) or _SETTINGS:GetMessageTime( MessageType ) - end - - --- Sets A2G LL DMS - -- @param #SETTINGS self - -- @return #SETTINGS - function SETTINGS:SetA2G_LL_DMS() - self.A2GSystem = "LL DMS" - end - - --- Sets A2G LL DDM - -- @param #SETTINGS self - -- @return #SETTINGS - function SETTINGS:SetA2G_LL_DDM() - self.A2GSystem = "LL DDM" - end - - --- Is LL DMS - -- @param #SETTINGS self - -- @return #boolean true if LL DMS - function SETTINGS:IsA2G_LL_DMS() - return ( self.A2GSystem and self.A2GSystem == "LL DMS" ) or ( not self.A2GSystem and _SETTINGS:IsA2G_LL_DMS() ) - end - - --- Is LL DDM - -- @param #SETTINGS self - -- @return #boolean true if LL DDM - function SETTINGS:IsA2G_LL_DDM() - return ( self.A2GSystem and self.A2GSystem == "LL DDM" ) or ( not self.A2GSystem and _SETTINGS:IsA2G_LL_DDM() ) - end - - --- Sets A2G MGRS - -- @param #SETTINGS self - -- @return #SETTINGS - function SETTINGS:SetA2G_MGRS() - self.A2GSystem = "MGRS" - end - - --- Is MGRS - -- @param #SETTINGS self - -- @return #boolean true if MGRS - function SETTINGS:IsA2G_MGRS() - return ( self.A2GSystem and self.A2GSystem == "MGRS" ) or ( not self.A2GSystem and _SETTINGS:IsA2G_MGRS() ) - end - - --- Sets A2G BRA - -- @param #SETTINGS self - -- @return #SETTINGS - function SETTINGS:SetA2G_BR() - self.A2GSystem = "BR" - end - - --- Is BRA - -- @param #SETTINGS self - -- @return #boolean true if BRA - function SETTINGS:IsA2G_BR() - return ( self.A2GSystem and self.A2GSystem == "BR" ) or ( not self.A2GSystem and _SETTINGS:IsA2G_BR() ) - end - - --- Sets A2A BRA - -- @param #SETTINGS self - -- @return #SETTINGS - function SETTINGS:SetA2A_BRAA() - self.A2ASystem = "BRAA" - end - - --- Is BRA - -- @param #SETTINGS self - -- @return #boolean true if BRA - function SETTINGS:IsA2A_BRAA() - self:E( { BRA = ( self.A2ASystem and self.A2ASystem == "BRAA" ) or ( not self.A2ASystem and _SETTINGS:IsA2A_BRAA() ) } ) - return ( self.A2ASystem and self.A2ASystem == "BRAA" ) or ( not self.A2ASystem and _SETTINGS:IsA2A_BRAA() ) - end - - --- Sets A2A BULLS - -- @param #SETTINGS self - -- @return #SETTINGS - function SETTINGS:SetA2A_BULLS() - self.A2ASystem = "BULLS" - end - - --- Is BULLS - -- @param #SETTINGS self - -- @return #boolean true if BULLS - function SETTINGS:IsA2A_BULLS() - return ( self.A2ASystem and self.A2ASystem == "BULLS" ) or ( not self.A2ASystem and _SETTINGS:IsA2A_BULLS() ) - end - - --- Sets A2A LL DMS - -- @param #SETTINGS self - -- @return #SETTINGS - function SETTINGS:SetA2A_LL_DMS() - self.A2ASystem = "LL DMS" - end - - --- Sets A2A LL DDM - -- @param #SETTINGS self - -- @return #SETTINGS - function SETTINGS:SetA2A_LL_DDM() - self.A2ASystem = "LL DDM" - end - - --- Is LL DMS - -- @param #SETTINGS self - -- @return #boolean true if LL DMS - function SETTINGS:IsA2A_LL_DMS() - return ( self.A2ASystem and self.A2ASystem == "LL DMS" ) or ( not self.A2ASystem and _SETTINGS:IsA2A_LL_DMS() ) - end - - --- Is LL DDM - -- @param #SETTINGS self - -- @return #boolean true if LL DDM - function SETTINGS:IsA2A_LL_DDM() - return ( self.A2ASystem and self.A2ASystem == "LL DDM" ) or ( not self.A2ASystem and _SETTINGS:IsA2A_LL_DDM() ) - end - - --- Sets A2A MGRS - -- @param #SETTINGS self - -- @return #SETTINGS - function SETTINGS:SetA2A_MGRS() - self.A2ASystem = "MGRS" - end - - --- Is MGRS - -- @param #SETTINGS self - -- @return #boolean true if MGRS - function SETTINGS:IsA2A_MGRS() - return ( self.A2ASystem and self.A2ASystem == "MGRS" ) or ( not self.A2ASystem and _SETTINGS:IsA2A_MGRS() ) - end - - --- @param #SETTINGS self - -- @return #SETTINGS - function SETTINGS:SetSystemMenu( MenuGroup, RootMenu ) - - local MenuText = "System Settings" - - local MenuTime = timer.getTime() - - local SettingsMenu = MENU_GROUP:New( MenuGroup, MenuText, RootMenu ):SetTime( MenuTime ) - - local A2GCoordinateMenu = MENU_GROUP:New( MenuGroup, "A2G Coordinate System", SettingsMenu ):SetTime( MenuTime ) - - - if not self:IsA2G_LL_DMS() then - MENU_GROUP_COMMAND:New( MenuGroup, "Lat/Lon Degree Min Sec (LL DMS)", A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "LL DMS" ):SetTime( MenuTime ) - end - - if not self:IsA2G_LL_DDM() then - MENU_GROUP_COMMAND:New( MenuGroup, "Lat/Lon Degree Dec Min (LL DDM)", A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "LL DDM" ):SetTime( MenuTime ) - end - - if self:IsA2G_LL_DDM() then - MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 1", A2GCoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 2", A2GCoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 3", A2GCoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime ) - end - - if not self:IsA2G_BR() then - MENU_GROUP_COMMAND:New( MenuGroup, "Bearing, Range (BR)", A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "BR" ):SetTime( MenuTime ) - end - - if not self:IsA2G_MGRS() then - MENU_GROUP_COMMAND:New( MenuGroup, "Military Grid (MGRS)", A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "MGRS" ):SetTime( MenuTime ) - end - - if self:IsA2G_MGRS() then - MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 1", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 2", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 3", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 4", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 4 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 5", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 5 ):SetTime( MenuTime ) - end - - local A2ACoordinateMenu = MENU_GROUP:New( MenuGroup, "A2A Coordinate System", SettingsMenu ):SetTime( MenuTime ) - - if not self:IsA2A_LL_DMS() then - MENU_GROUP_COMMAND:New( MenuGroup, "Lat/Lon Degree Min Sec (LL DMS)", A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "LL DMS" ):SetTime( MenuTime ) - end - - if not self:IsA2A_LL_DDM() then - MENU_GROUP_COMMAND:New( MenuGroup, "Lat/Lon Degree Dec Min (LL DDM)", A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "LL DDM" ):SetTime( MenuTime ) - end - - if self:IsA2A_LL_DDM() then - MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 1", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 2", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 3", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime ) - end - - if not self:IsA2A_BULLS() then - MENU_GROUP_COMMAND:New( MenuGroup, "Bullseye (BULLS)", A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "BULLS" ):SetTime( MenuTime ) - end - - if not self:IsA2A_BRAA() then - MENU_GROUP_COMMAND:New( MenuGroup, "Bearing Range Altitude Aspect (BRAA)", A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "BRAA" ):SetTime( MenuTime ) - end - - if not self:IsA2A_MGRS() then - MENU_GROUP_COMMAND:New( MenuGroup, "Military Grid (MGRS)", A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "MGRS" ):SetTime( MenuTime ) - end - - if self:IsA2A_MGRS() then - MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 1", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 2", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 3", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 4", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 4 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 5", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 5 ):SetTime( MenuTime ) - end - - local MetricsMenu = MENU_GROUP:New( MenuGroup, "Measures and Weights System", SettingsMenu ):SetTime( MenuTime ) - - if self:IsMetric() then - MENU_GROUP_COMMAND:New( MenuGroup, "Imperial (Miles,Feet)", MetricsMenu, self.MenuMWSystem, self, MenuGroup, RootMenu, false ):SetTime( MenuTime ) - end - - if self:IsImperial() then - MENU_GROUP_COMMAND:New( MenuGroup, "Metric (Kilometers,Meters)", MetricsMenu, self.MenuMWSystem, self, MenuGroup, RootMenu, true ):SetTime( MenuTime ) - end - - local MessagesMenu = MENU_GROUP:New( MenuGroup, "Messages and Reports", SettingsMenu ):SetTime( MenuTime ) - - local UpdateMessagesMenu = MENU_GROUP:New( MenuGroup, "Update Messages", MessagesMenu ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "Off", UpdateMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Update, 0 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "5 seconds", UpdateMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Update, 5 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "10 seconds", UpdateMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Update, 10 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "15 seconds", UpdateMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Update, 15 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "30 seconds", UpdateMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Update, 30 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "1 minute", UpdateMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Update, 60 ):SetTime( MenuTime ) - - local InformationMessagesMenu = MENU_GROUP:New( MenuGroup, "Information Messages", MessagesMenu ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "5 seconds", InformationMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Information, 5 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "10 seconds", InformationMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Information, 10 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "15 seconds", InformationMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Information, 15 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "30 seconds", InformationMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Information, 30 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "1 minute", InformationMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Information, 60 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "2 minutes", InformationMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Information, 120 ):SetTime( MenuTime ) - - local BriefingReportsMenu = MENU_GROUP:New( MenuGroup, "Briefing Reports", MessagesMenu ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "15 seconds", BriefingReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Briefing, 15 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "30 seconds", BriefingReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Briefing, 30 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "1 minute", BriefingReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Briefing, 60 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "2 minutes", BriefingReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Briefing, 120 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "3 minutes", BriefingReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Briefing, 180 ):SetTime( MenuTime ) - - local OverviewReportsMenu = MENU_GROUP:New( MenuGroup, "Overview Reports", MessagesMenu ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "15 seconds", OverviewReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Overview, 15 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "30 seconds", OverviewReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Overview, 30 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "1 minute", OverviewReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Overview, 60 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "2 minutes", OverviewReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Overview, 120 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "3 minutes", OverviewReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Overview, 180 ):SetTime( MenuTime ) - - local DetailedReportsMenu = MENU_GROUP:New( MenuGroup, "Detailed Reports", MessagesMenu ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "15 seconds", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 15 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "30 seconds", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 30 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "1 minute", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 60 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "2 minutes", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 120 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "3 minutes", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 180 ):SetTime( MenuTime ) - - - SettingsMenu:Remove( MenuTime ) - - return self - end - - --- @param #SETTINGS self - -- @param RootMenu - -- @param Wrapper.Client#CLIENT PlayerUnit - -- @param #string MenuText - -- @return #SETTINGS - function SETTINGS:SetPlayerMenu( PlayerUnit ) - - local PlayerGroup = PlayerUnit:GetGroup() - local PlayerName = PlayerUnit:GetPlayerName() - local PlayerNames = PlayerGroup:GetPlayerNames() - - local PlayerMenu = MENU_GROUP:New( PlayerGroup, 'Settings "' .. PlayerName .. '"' ) - - self.PlayerMenu = PlayerMenu - - local A2GCoordinateMenu = MENU_GROUP:New( PlayerGroup, "A2G Coordinate System", PlayerMenu ) - - if not self:IsA2G_LL_DMS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Lat/Lon Degree Min Sec (LL DMS)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DMS" ) - end - - if not self:IsA2G_LL_DDM() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Lat/Lon Degree Dec Min (LL DDM)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DDM" ) - end - - if self:IsA2G_LL_DDM() then - MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 1", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 2", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 3", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 ) - end - - if not self:IsA2G_BR() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Bearing, Range (BR)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "BR" ) - end - - if not self:IsA2G_MGRS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "MGRS" ) - end - - if self:IsA2G_MGRS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "MGRS Accuracy 1", A2GCoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "MGRS Accuracy 2", A2GCoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "MGRS Accuracy 3", A2GCoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "MGRS Accuracy 4", A2GCoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 4 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "MGRS Accuracy 5", A2GCoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 5 ) - end - - local A2ACoordinateMenu = MENU_GROUP:New( PlayerGroup, "A2A Coordinate System", PlayerMenu ) - - - if not self:IsA2A_LL_DMS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Lat/Lon Degree Min Sec (LL DMS)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DMS" ) - end - - if not self:IsA2A_LL_DDM() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Lat/Lon Degree Dec Min (LL DDM)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DDM" ) - end - - if self:IsA2A_LL_DDM() then - MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 1", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 2", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 3", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 ) - end - - if not self:IsA2A_BULLS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Bullseye (BULLS)", A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "BULLS" ) - end - - if not self:IsA2A_BRAA() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Bearing Range Altitude Aspect (BRAA)", A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "BRAA" ) - end - - if not self:IsA2A_MGRS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS)", A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "MGRS" ) - end - - if self:IsA2A_MGRS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS) Accuracy 1", A2ACoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS) Accuracy 2", A2ACoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS) Accuracy 3", A2ACoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS) Accuracy 4", A2ACoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 4 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS) Accuracy 5", A2ACoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 5 ) - end - - local MetricsMenu = MENU_GROUP:New( PlayerGroup, "Measures and Weights System", PlayerMenu ) - - if self:IsMetric() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Imperial (Miles,Feet)", MetricsMenu, self.MenuGroupMWSystem, self, PlayerUnit, PlayerGroup, PlayerName, false ) - end - - if self:IsImperial() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Metric (Kilometers,Meters)", MetricsMenu, self.MenuGroupMWSystem, self, PlayerUnit, PlayerGroup, PlayerName, true ) - end - - - local MessagesMenu = MENU_GROUP:New( PlayerGroup, "Messages and Reports", PlayerMenu ) - - local UpdateMessagesMenu = MENU_GROUP:New( PlayerGroup, "Update Messages", MessagesMenu ) - MENU_GROUP_COMMAND:New( PlayerGroup, "Off", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 0 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "5 seconds", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 5 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "10 seconds", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 10 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "15 seconds", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 15 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "30 seconds", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 30 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "1 minute", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 60 ) - - local InformationMessagesMenu = MENU_GROUP:New( PlayerGroup, "Information Messages", MessagesMenu ) - MENU_GROUP_COMMAND:New( PlayerGroup, "5 seconds", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 5 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "10 seconds", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 10 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "15 seconds", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 15 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "30 seconds", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 30 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "1 minute", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 60 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "2 minutes", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 120 ) - - local BriefingReportsMenu = MENU_GROUP:New( PlayerGroup, "Briefing Reports", MessagesMenu ) - MENU_GROUP_COMMAND:New( PlayerGroup, "15 seconds", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 15 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "30 seconds", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 30 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "1 minute", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 60 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "2 minutes", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 120 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "3 minutes", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 180 ) - - local OverviewReportsMenu = MENU_GROUP:New( PlayerGroup, "Overview Reports", MessagesMenu ) - MENU_GROUP_COMMAND:New( PlayerGroup, "15 seconds", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 15 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "30 seconds", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 30 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "1 minute", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 60 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "2 minutes", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 120 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "3 minutes", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 180 ) - - local DetailedReportsMenu = MENU_GROUP:New( PlayerGroup, "Detailed Reports", MessagesMenu ) - MENU_GROUP_COMMAND:New( PlayerGroup, "15 seconds", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 15 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "30 seconds", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 30 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "1 minute", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 60 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "2 minutes", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 120 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "3 minutes", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 180 ) - - - return self - end - - --- @param #SETTINGS self - -- @param RootMenu - -- @param Wrapper.Client#CLIENT PlayerUnit - -- @return #SETTINGS - function SETTINGS:RemovePlayerMenu( PlayerUnit ) - - if self.PlayerMenu then - self.PlayerMenu:Remove() - end - - return self - end - - - --- @param #SETTINGS self - function SETTINGS:A2GMenuSystem( MenuGroup, RootMenu, A2GSystem ) - self.A2GSystem = A2GSystem - MESSAGE:New( string.format("Settings: Default A2G coordinate system set to %s for all players!", A2GSystem ), 5 ):ToAll() - self:SetSystemMenu( MenuGroup, RootMenu ) - end - - --- @param #SETTINGS self - function SETTINGS:A2AMenuSystem( MenuGroup, RootMenu, A2ASystem ) - self.A2ASystem = A2ASystem - MESSAGE:New( string.format("Settings: Default A2A coordinate system set to %s for all players!", A2ASystem ), 5 ):ToAll() - self:SetSystemMenu( MenuGroup, RootMenu ) - end - - --- @param #SETTINGS self - function SETTINGS:MenuLL_DDM_Accuracy( MenuGroup, RootMenu, LL_Accuracy ) - self.LL_Accuracy = LL_Accuracy - MESSAGE:New( string.format("Settings: Default LL accuracy set to %s for all players!", LL_Accuracy ), 5 ):ToAll() - self:SetSystemMenu( MenuGroup, RootMenu ) - end - - --- @param #SETTINGS self - function SETTINGS:MenuMGRS_Accuracy( MenuGroup, RootMenu, MGRS_Accuracy ) - self.MGRS_Accuracy = MGRS_Accuracy - MESSAGE:New( string.format("Settings: Default MGRS accuracy set to %s for all players!", MGRS_Accuracy ), 5 ):ToAll() - self:SetSystemMenu( MenuGroup, RootMenu ) - end - - --- @param #SETTINGS self - function SETTINGS:MenuMWSystem( MenuGroup, RootMenu, MW ) - self.Metric = MW - MESSAGE:New( string.format("Settings: Default measurement format set to %s for all players!", MW and "Metric" or "Imperial" ), 5 ):ToAll() - self:SetSystemMenu( MenuGroup, RootMenu ) - end - - --- @param #SETTINGS self - function SETTINGS:MenuMessageTimingsSystem( MenuGroup, RootMenu, MessageType, MessageTime ) - self:SetMessageTime( MessageType, MessageTime ) - MESSAGE:New( string.format( "Settings: Default message time set for %s to %d.", MessageType, MessageTime ), 5 ):ToAll() - end - - do - --- @param #SETTINGS self - function SETTINGS:MenuGroupA2GSystem( PlayerUnit, PlayerGroup, PlayerName, A2GSystem ) - BASE:E( {self, PlayerUnit:GetName(), A2GSystem} ) - self.A2GSystem = A2GSystem - MESSAGE:New( string.format( "Settings: A2G format set to %s for player %s.", A2GSystem, PlayerName ), 5 ):ToGroup( PlayerGroup ) - self:RemovePlayerMenu(PlayerUnit) - self:SetPlayerMenu(PlayerUnit) - end - - --- @param #SETTINGS self - function SETTINGS:MenuGroupA2ASystem( PlayerUnit, PlayerGroup, PlayerName, A2ASystem ) - self.A2ASystem = A2ASystem - MESSAGE:New( string.format( "Settings: A2A format set to %s for player %s.", A2ASystem, PlayerName ), 5 ):ToGroup( PlayerGroup ) - self:RemovePlayerMenu(PlayerUnit) - self:SetPlayerMenu(PlayerUnit) - end - - --- @param #SETTINGS self - function SETTINGS:MenuGroupLL_DDM_AccuracySystem( PlayerUnit, PlayerGroup, PlayerName, LL_Accuracy ) - self.LL_Accuracy = LL_Accuracy - MESSAGE:New( string.format( "Settings: A2G LL format accuracy set to %d for player %s.", LL_Accuracy, PlayerName ), 5 ):ToGroup( PlayerGroup ) - self:RemovePlayerMenu(PlayerUnit) - self:SetPlayerMenu(PlayerUnit) - end - - --- @param #SETTINGS self - function SETTINGS:MenuGroupMGRS_AccuracySystem( PlayerUnit, PlayerGroup, PlayerName, MGRS_Accuracy ) - self.MGRS_Accuracy = MGRS_Accuracy - MESSAGE:New( string.format( "Settings: A2G MGRS format accuracy set to %d for player %s.", MGRS_Accuracy, PlayerName ), 5 ):ToGroup( PlayerGroup ) - self:RemovePlayerMenu(PlayerUnit) - self:SetPlayerMenu(PlayerUnit) - end - - --- @param #SETTINGS self - function SETTINGS:MenuGroupMWSystem( PlayerUnit, PlayerGroup, PlayerName, MW ) - self.Metric = MW - MESSAGE:New( string.format( "Settings: Measurement format set to %s for player %s.", MW and "Metric" or "Imperial", PlayerName ), 5 ):ToGroup( PlayerGroup ) - self:RemovePlayerMenu(PlayerUnit) - self:SetPlayerMenu(PlayerUnit) - end - - --- @param #SETTINGS self - function SETTINGS:MenuGroupMessageTimingsSystem( PlayerUnit, PlayerGroup, PlayerName, MessageType, MessageTime ) - self:SetMessageTime( MessageType, MessageTime ) - MESSAGE:New( string.format( "Settings: Default message time set for %s to %d.", MessageType, MessageTime ), 5 ):ToGroup( PlayerGroup ) - end - - end - -end - - ---- **Core** -- MENU_ classes model the definition of **hierarchical menu structures** and **commands for players** within a mission. --- --- === --- --- DCS Menus can be managed using the MENU classes. --- The advantage of using MENU classes is that it hides the complexity of dealing with menu management in more advanced scanerios where you need to --- set menus and later remove them, and later set them again. You'll find while using use normal DCS scripting functions, that setting and removing --- menus is not a easy feat if you have complex menu hierarchies defined. --- Using the MOOSE menu classes, the removal and refreshing of menus are nicely being handled within these classes, and becomes much more easy. --- On top, MOOSE implements **variable parameter** passing for command menus. --- --- There are basically two different MENU class types that you need to use: --- --- ### To manage **main menus**, the classes begin with **MENU_**: --- --- * @{Menu#MENU_MISSION}: Manages main menus for whole mission file. --- * @{Menu#MENU_COALITION}: Manages main menus for whole coalition. --- * @{Menu#MENU_GROUP}: Manages main menus for GROUPs. --- * @{Menu#MENU_CLIENT}: Manages main menus for CLIENTs. This manages menus for units with the skill level "Client". --- --- ### To manage **command menus**, which are menus that allow the player to issue **functions**, the classes begin with **MENU_COMMAND_**: --- --- * @{Menu#MENU_MISSION_COMMAND}: Manages command menus for whole mission file. --- * @{Menu#MENU_COALITION_COMMAND}: Manages command menus for whole coalition. --- * @{Menu#MENU_GROUP_COMMAND}: Manages command menus for GROUPs. --- * @{Menu#MENU_CLIENT_COMMAND}: Manages command menus for CLIENTs. This manages menus for units with the skill level "Client". --- --- === ---- --- ### Author: **Sven Van de Velde (FlightControl)** --- ### Contributions: --- --- ==== --- --- @module Menu - - -do -- MENU_BASE - - --- @type MENU_BASE - -- @extends Base#BASE - - --- # MENU_BASE class, extends @{Base#BASE} - -- The MENU_BASE class defines the main MENU class where other MENU classes are derived from. - -- This is an abstract class, so don't use it. - -- @field #MENU_BASE - MENU_BASE = { - ClassName = "MENU_BASE", - MenuPath = nil, - MenuText = "", - MenuParentPath = nil - } - - --- Consructor - -- @param #MENU_BASE - -- @return #MENU_BASE - function MENU_BASE:New( MenuText, ParentMenu ) - - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, BASE:New() ) - - self.MenuPath = nil - self.MenuText = MenuText - self.MenuParentPath = MenuParentPath - self.Menus = {} - self.MenuCount = 0 - self.MenuRemoveParent = false - self.MenuTime = timer.getTime() - - return self - end - - --- Gets a @{Menu} from a parent @{Menu} - -- @param #MENU_BASE self - -- @param #string MenuText The text of the child menu. - -- @return #MENU_BASE - function MENU_BASE:GetMenu( MenuText ) - self:F2( { Menu = self.Menus[MenuText] } ) - return self.Menus[MenuText] - end - - --- Sets a @{Menu} to remove automatically the parent menu when the menu removed is the last child menu of that parent @{Menu}. - -- @param #MENU_BASE self - -- @param #boolean RemoveParent If true, the parent menu is automatically removed when this menu is the last child menu of that parent @{Menu}. - -- @return #MENU_BASE - function MENU_BASE:SetRemoveParent( RemoveParent ) - self:F2( { RemoveParent } ) - self.MenuRemoveParent = RemoveParent - return self - end - - - --- Sets a time stamp for later prevention of menu removal. - -- @param #MENU_BASE self - -- @param MenuTime - -- @return #MENU_BASE - function MENU_BASE:SetTime( MenuTime ) - self.MenuTime = MenuTime - return self - end - - --- Sets a tag for later selection of menu refresh. - -- @param #MENU_BASE self - -- @param #string MenuTag A Tag or Key that will filter only menu items set with this key. - -- @return #MENU_BASE - function MENU_BASE:SetTag( MenuTag ) - self.MenuTag = MenuTag - return self - end - -end - -do -- MENU_COMMAND_BASE - - --- @type MENU_COMMAND_BASE - -- @field #function MenuCallHandler - -- @extends Core.Menu#MENU_BASE - - --- # MENU_COMMAND_BASE class, extends @{Base#BASE} - -- ---------------------------------------------------------- - -- The MENU_COMMAND_BASE class defines the main MENU class where other MENU COMMAND_ - -- classes are derived from, in order to set commands. - -- - -- @field #MENU_COMMAND_BASE - MENU_COMMAND_BASE = { - ClassName = "MENU_COMMAND_BASE", - CommandMenuFunction = nil, - CommandMenuArgument = nil, - MenuCallHandler = nil, - } - - --- Constructor - -- @param #MENU_COMMAND_BASE - -- @return #MENU_COMMAND_BASE - function MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, CommandMenuArguments ) - - local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) -- #MENU_COMMAND_BASE - - -- When a menu function goes into error, DCS displays an obscure menu message. - -- This error handler catches the menu error and displays the full call stack. - local ErrorHandler = function( errmsg ) - env.info( "MOOSE error in MENU COMMAND function: " .. errmsg ) - if debug ~= nil then - env.info( debug.traceback() ) - end - return errmsg - end - - self:SetCommandMenuFunction( CommandMenuFunction ) - self:SetCommandMenuArguments( CommandMenuArguments ) - self.MenuCallHandler = function() - local function MenuFunction() - return self.CommandMenuFunction( unpack( self.CommandMenuArguments ) ) - end - local Status, Result = xpcall( MenuFunction, ErrorHandler ) - end - - return self - end - - --- This sets the new command function of a menu, - -- so that if a menu is regenerated, or if command function changes, - -- that the function set for the menu is loosely coupled with the menu itself!!! - -- If the function changes, no new menu needs to be generated if the menu text is the same!!! - -- @param #MENU_COMMAND_BASE - -- @return #MENU_COMMAND_BASE - function MENU_COMMAND_BASE:SetCommandMenuFunction( CommandMenuFunction ) - self.CommandMenuFunction = CommandMenuFunction - return self - end - - --- This sets the new command arguments of a menu, - -- so that if a menu is regenerated, or if command arguments change, - -- that the arguments set for the menu are loosely coupled with the menu itself!!! - -- If the arguments change, no new menu needs to be generated if the menu text is the same!!! - -- @param #MENU_COMMAND_BASE - -- @return #MENU_COMMAND_BASE - function MENU_COMMAND_BASE:SetCommandMenuArguments( CommandMenuArguments ) - self.CommandMenuArguments = CommandMenuArguments - return self - end - -end - - -do -- MENU_MISSION - - --- @type MENU_MISSION - -- @extends Core.Menu#MENU_BASE - - --- # MENU_MISSION class, extends @{Menu#MENU_BASE} - -- - -- The MENU_MISSION class manages the main menus for a complete mission. - -- You can add menus with the @{#MENU_MISSION.New} method, which constructs a MENU_MISSION object and returns you the object reference. - -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_MISSION.Remove}. - -- @field #MENU_MISSION - MENU_MISSION = { - ClassName = "MENU_MISSION" - } - - --- MENU_MISSION constructor. Creates a new MENU_MISSION object and creates the menu for a complete mission file. - -- @param #MENU_MISSION self - -- @param #string MenuText The text for the menu. - -- @param #table ParentMenu The parent menu. This parameter can be ignored if you want the menu to be located at the perent menu of DCS world (under F10 other). - -- @return #MENU_MISSION - function MENU_MISSION:New( MenuText, ParentMenu ) - - local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) - - self:F( { MenuText, ParentMenu } ) - - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self.Menus = {} - - self:T( { MenuText } ) - - self.MenuPath = missionCommands.addSubMenu( MenuText, self.MenuParentPath ) - - self:T( { self.MenuPath } ) - - if ParentMenu and ParentMenu.Menus then - ParentMenu.Menus[self.MenuPath] = self - end - - return self - end - - --- Removes the sub menus recursively of this MENU_MISSION. Note that the main menu is kept! - -- @param #MENU_MISSION self - -- @return #MENU_MISSION - function MENU_MISSION:RemoveSubMenus() - self:F( self.MenuPath ) - - for MenuID, Menu in pairs( self.Menus ) do - Menu:Remove() - end - - end - - --- Removes the main menu and the sub menus recursively of this MENU_MISSION. - -- @param #MENU_MISSION self - -- @return #nil - function MENU_MISSION:Remove() - self:F( self.MenuPath ) - - self:RemoveSubMenus() - missionCommands.removeItem( self.MenuPath ) - if self.ParentMenu then - self.ParentMenu.Menus[self.MenuPath] = nil - end - - return nil - end - -end - -do -- MENU_MISSION_COMMAND - - --- @type MENU_MISSION_COMMAND - -- @extends Core.Menu#MENU_COMMAND_BASE - - --- # MENU_MISSION_COMMAND class, extends @{Menu#MENU_COMMAND_BASE} - -- - -- The MENU_MISSION_COMMAND class manages the command menus for a complete mission, which allow players to execute functions during mission execution. - -- You can add menus with the @{#MENU_MISSION_COMMAND.New} method, which constructs a MENU_MISSION_COMMAND object and returns you the object reference. - -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_MISSION_COMMAND.Remove}. - -- - -- @field #MENU_MISSION_COMMAND - MENU_MISSION_COMMAND = { - ClassName = "MENU_MISSION_COMMAND" - } - - --- MENU_MISSION constructor. Creates a new radio command item for a complete mission file, which can invoke a function with parameters. - -- @param #MENU_MISSION_COMMAND self - -- @param #string MenuText The text for the menu. - -- @param Menu#MENU_MISSION ParentMenu The parent menu. - -- @param CommandMenuFunction A function that is called when the menu key is pressed. - -- @param CommandMenuArgument An argument for the function. There can only be ONE argument given. So multiple arguments must be wrapped into a table. See the below example how to do this. - -- @return #MENU_MISSION_COMMAND self - function MENU_MISSION_COMMAND:New( MenuText, ParentMenu, CommandMenuFunction, ... ) - - local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) - - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self:T( { MenuText, CommandMenuFunction, arg } ) - - - self.MenuPath = missionCommands.addCommand( MenuText, self.MenuParentPath, self.MenuCallHandler ) - - ParentMenu.Menus[self.MenuPath] = self - - return self - end - - --- Removes a radio command item for a coalition - -- @param #MENU_MISSION_COMMAND self - -- @return #nil - function MENU_MISSION_COMMAND:Remove() - self:F( self.MenuPath ) - - missionCommands.removeItem( self.MenuPath ) - if self.ParentMenu then - self.ParentMenu.Menus[self.MenuPath] = nil - end - return nil - end - -end - - - -do -- MENU_COALITION - - --- @type MENU_COALITION - -- @extends Core.Menu#MENU_BASE - - --- # MENU_COALITION class, extends @{Menu#MENU_BASE} - -- - -- The @{Menu#MENU_COALITION} class manages the main menus for coalitions. - -- You can add menus with the @{#MENU_COALITION.New} method, which constructs a MENU_COALITION object and returns you the object reference. - -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_COALITION.Remove}. - -- - -- - -- @usage - -- -- This demo creates a menu structure for the planes within the red coalition. - -- -- To test, join the planes, then look at the other radio menus (Option F10). - -- -- Then switch planes and check if the menu is still there. - -- - -- local Plane1 = CLIENT:FindByName( "Plane 1" ) - -- local Plane2 = CLIENT:FindByName( "Plane 2" ) - -- - -- - -- -- This would create a menu for the red coalition under the main DCS "Others" menu. - -- local MenuCoalitionRed = MENU_COALITION:New( coalition.side.RED, "Manage Menus" ) - -- - -- - -- local function ShowStatus( StatusText, Coalition ) - -- - -- MESSAGE:New( Coalition, 15 ):ToRed() - -- Plane1:Message( StatusText, 15 ) - -- Plane2:Message( StatusText, 15 ) - -- end - -- - -- local MenuStatus -- Menu#MENU_COALITION - -- local MenuStatusShow -- Menu#MENU_COALITION_COMMAND - -- - -- local function RemoveStatusMenu() - -- MenuStatus:Remove() - -- end - -- - -- local function AddStatusMenu() - -- - -- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object. - -- MenuStatus = MENU_COALITION:New( coalition.side.RED, "Status for Planes" ) - -- MenuStatusShow = MENU_COALITION_COMMAND:New( coalition.side.RED, "Show Status", MenuStatus, ShowStatus, "Status of planes is ok!", "Message to Red Coalition" ) - -- end - -- - -- local MenuAdd = MENU_COALITION_COMMAND:New( coalition.side.RED, "Add Status Menu", MenuCoalitionRed, AddStatusMenu ) - -- local MenuRemove = MENU_COALITION_COMMAND:New( coalition.side.RED, "Remove Status Menu", MenuCoalitionRed, RemoveStatusMenu ) - -- - -- @field #MENU_COALITION - MENU_COALITION = { - ClassName = "MENU_COALITION" - } - - --- MENU_COALITION constructor. Creates a new MENU_COALITION object and creates the menu for a complete coalition. - -- @param #MENU_COALITION self - -- @param Dcs.DCSCoalition#coalition.side Coalition The coalition owning the menu. - -- @param #string MenuText The text for the menu. - -- @param #table ParentMenu The parent menu. This parameter can be ignored if you want the menu to be located at the perent menu of DCS world (under F10 other). - -- @return #MENU_COALITION self - function MENU_COALITION:New( Coalition, MenuText, ParentMenu ) - - local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) - - self:F( { Coalition, MenuText, ParentMenu } ) - - self.Coalition = Coalition - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self.Menus = {} - - self:T( { MenuText } ) - - self.MenuPath = missionCommands.addSubMenuForCoalition( Coalition, MenuText, self.MenuParentPath ) - - self:T( { self.MenuPath } ) - - if ParentMenu and ParentMenu.Menus then - ParentMenu.Menus[self.MenuPath] = self - end - - return self - end - - --- Removes the sub menus recursively of this MENU_COALITION. Note that the main menu is kept! - -- @param #MENU_COALITION self - -- @return #MENU_COALITION - function MENU_COALITION:RemoveSubMenus() - self:F( self.MenuPath ) - - for MenuID, Menu in pairs( self.Menus ) do - Menu:Remove() - end - - end - - --- Removes the main menu and the sub menus recursively of this MENU_COALITION. - -- @param #MENU_COALITION self - -- @return #nil - function MENU_COALITION:Remove() - self:F( self.MenuPath ) - - self:RemoveSubMenus() - missionCommands.removeItemForCoalition( self.Coalition, self.MenuPath ) - if self.ParentMenu then - self.ParentMenu.Menus[self.MenuPath] = nil - end - - return nil - end - -end - -do -- MENU_COALITION_COMMAND - - --- @type MENU_COALITION_COMMAND - -- @extends Core.Menu#MENU_COMMAND_BASE - - --- # MENU_COALITION_COMMAND class, extends @{Menu#MENU_COMMAND_BASE} - -- - -- The MENU_COALITION_COMMAND class manages the command menus for coalitions, which allow players to execute functions during mission execution. - -- You can add menus with the @{#MENU_COALITION_COMMAND.New} method, which constructs a MENU_COALITION_COMMAND object and returns you the object reference. - -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_COALITION_COMMAND.Remove}. - -- - -- @field #MENU_COALITION_COMMAND - MENU_COALITION_COMMAND = { - ClassName = "MENU_COALITION_COMMAND" - } - - --- MENU_COALITION constructor. Creates a new radio command item for a coalition, which can invoke a function with parameters. - -- @param #MENU_COALITION_COMMAND self - -- @param Dcs.DCSCoalition#coalition.side Coalition The coalition owning the menu. - -- @param #string MenuText The text for the menu. - -- @param Menu#MENU_COALITION ParentMenu The parent menu. - -- @param CommandMenuFunction A function that is called when the menu key is pressed. - -- @param CommandMenuArgument An argument for the function. There can only be ONE argument given. So multiple arguments must be wrapped into a table. See the below example how to do this. - -- @return #MENU_COALITION_COMMAND - function MENU_COALITION_COMMAND:New( Coalition, MenuText, ParentMenu, CommandMenuFunction, ... ) - - local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) - - self.MenuCoalition = Coalition - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self:T( { MenuText, CommandMenuFunction, arg } ) - - - self.MenuPath = missionCommands.addCommandForCoalition( self.MenuCoalition, MenuText, self.MenuParentPath, self.MenuCallHandler ) - - ParentMenu.Menus[self.MenuPath] = self - - return self - end - - --- Removes a radio command item for a coalition - -- @param #MENU_COALITION_COMMAND self - -- @return #nil - function MENU_COALITION_COMMAND:Remove() - self:F( self.MenuPath ) - - missionCommands.removeItemForCoalition( self.MenuCoalition, self.MenuPath ) - if self.ParentMenu then - self.ParentMenu.Menus[self.MenuPath] = nil - end - return nil - end - -end - -do -- MENU_CLIENT - - -- This local variable is used to cache the menus registered under clients. - -- Menus don't dissapear when clients are destroyed and restarted. - -- So every menu for a client created must be tracked so that program logic accidentally does not create - -- the same menus twice during initialization logic. - -- These menu classes are handling this logic with this variable. - local _MENUCLIENTS = {} - - --- MENU_COALITION constructor. Creates a new radio command item for a coalition, which can invoke a function with parameters. - -- @type MENU_CLIENT - -- @extends Core.Menu#MENU_BASE - - - --- # MENU_CLIENT class, extends @{Menu#MENU_BASE} - -- - -- The MENU_CLIENT class manages the main menus for coalitions. - -- You can add menus with the @{#MENU_CLIENT.New} method, which constructs a MENU_CLIENT object and returns you the object reference. - -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_CLIENT.Remove}. - -- - -- @usage - -- -- This demo creates a menu structure for the two clients of planes. - -- -- Each client will receive a different menu structure. - -- -- To test, join the planes, then look at the other radio menus (Option F10). - -- -- Then switch planes and check if the menu is still there. - -- -- And play with the Add and Remove menu options. - -- - -- -- Note that in multi player, this will only work after the DCS clients bug is solved. - -- - -- local function ShowStatus( PlaneClient, StatusText, Coalition ) - -- - -- MESSAGE:New( Coalition, 15 ):ToRed() - -- PlaneClient:Message( StatusText, 15 ) - -- end - -- - -- local MenuStatus = {} - -- - -- local function RemoveStatusMenu( MenuClient ) - -- local MenuClientName = MenuClient:GetName() - -- MenuStatus[MenuClientName]:Remove() - -- end - -- - -- --- @param Wrapper.Client#CLIENT MenuClient - -- local function AddStatusMenu( MenuClient ) - -- local MenuClientName = MenuClient:GetName() - -- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object. - -- MenuStatus[MenuClientName] = MENU_CLIENT:New( MenuClient, "Status for Planes" ) - -- MENU_CLIENT_COMMAND:New( MenuClient, "Show Status", MenuStatus[MenuClientName], ShowStatus, MenuClient, "Status of planes is ok!", "Message to Red Coalition" ) - -- end - -- - -- SCHEDULER:New( nil, - -- function() - -- local PlaneClient = CLIENT:FindByName( "Plane 1" ) - -- if PlaneClient and PlaneClient:IsAlive() then - -- local MenuManage = MENU_CLIENT:New( PlaneClient, "Manage Menus" ) - -- MENU_CLIENT_COMMAND:New( PlaneClient, "Add Status Menu Plane 1", MenuManage, AddStatusMenu, PlaneClient ) - -- MENU_CLIENT_COMMAND:New( PlaneClient, "Remove Status Menu Plane 1", MenuManage, RemoveStatusMenu, PlaneClient ) - -- end - -- end, {}, 10, 10 ) - -- - -- SCHEDULER:New( nil, - -- function() - -- local PlaneClient = CLIENT:FindByName( "Plane 2" ) - -- if PlaneClient and PlaneClient:IsAlive() then - -- local MenuManage = MENU_CLIENT:New( PlaneClient, "Manage Menus" ) - -- MENU_CLIENT_COMMAND:New( PlaneClient, "Add Status Menu Plane 2", MenuManage, AddStatusMenu, PlaneClient ) - -- MENU_CLIENT_COMMAND:New( PlaneClient, "Remove Status Menu Plane 2", MenuManage, RemoveStatusMenu, PlaneClient ) - -- end - -- end, {}, 10, 10 ) - -- - -- @field #MENU_CLIENT - MENU_CLIENT = { - ClassName = "MENU_CLIENT" - } - - --- MENU_CLIENT constructor. Creates a new radio menu item for a client. - -- @param #MENU_CLIENT self - -- @param Wrapper.Client#CLIENT Client The Client owning the menu. - -- @param #string MenuText The text for the menu. - -- @param #table ParentMenu The parent menu. - -- @return #MENU_CLIENT self - function MENU_CLIENT:New( Client, MenuText, ParentMenu ) - - -- Arrange meta tables - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU_BASE:New( MenuText, MenuParentPath ) ) - self:F( { Client, MenuText, ParentMenu } ) - - self.MenuClient = Client - self.MenuClientGroupID = Client:GetClientGroupID() - self.MenuParentPath = MenuParentPath - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self.Menus = {} - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - self:T( { Client:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText } ) - - local MenuPathID = table.concat(MenuParentPath) .. "/" .. MenuText - if MenuPath[MenuPathID] then - missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), MenuPath[MenuPathID] ) - end - - self.MenuPath = missionCommands.addSubMenuForGroup( self.MenuClient:GetClientGroupID(), MenuText, MenuParentPath ) - MenuPath[MenuPathID] = self.MenuPath - - self:T( { Client:GetClientGroupName(), self.MenuPath } ) - - if ParentMenu and ParentMenu.Menus then - ParentMenu.Menus[self.MenuPath] = self - end - return self - end - - --- Removes the sub menus recursively of this @{#MENU_CLIENT}. - -- @param #MENU_CLIENT self - -- @return #MENU_CLIENT self - function MENU_CLIENT:RemoveSubMenus() - self:F( self.MenuPath ) - - for MenuID, Menu in pairs( self.Menus ) do - Menu:Remove() - end - - end - - --- Removes the sub menus recursively of this MENU_CLIENT. - -- @param #MENU_CLIENT self - -- @return #nil - function MENU_CLIENT:Remove() - self:F( self.MenuPath ) - - self:RemoveSubMenus() - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - if MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] then - MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] = nil - end - - missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), self.MenuPath ) - self.ParentMenu.Menus[self.MenuPath] = nil - return nil - end - - - --- @type MENU_CLIENT_COMMAND - -- @extends Core.Menu#MENU_COMMAND - - --- # MENU_CLIENT_COMMAND class, extends @{Menu#MENU_COMMAND_BASE} - -- - -- The MENU_CLIENT_COMMAND class manages the command menus for coalitions, which allow players to execute functions during mission execution. - -- You can add menus with the @{#MENU_CLIENT_COMMAND.New} method, which constructs a MENU_CLIENT_COMMAND object and returns you the object reference. - -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_CLIENT_COMMAND.Remove}. - -- - -- @field #MENU_CLIENT_COMMAND - MENU_CLIENT_COMMAND = { - ClassName = "MENU_CLIENT_COMMAND" - } - - --- MENU_CLIENT_COMMAND constructor. Creates a new radio command item for a client, which can invoke a function with parameters. - -- @param #MENU_CLIENT_COMMAND self - -- @param Wrapper.Client#CLIENT Client The Client owning the menu. - -- @param #string MenuText The text for the menu. - -- @param #MENU_BASE ParentMenu The parent menu. - -- @param CommandMenuFunction A function that is called when the menu key is pressed. - -- @return Menu#MENU_CLIENT_COMMAND self - function MENU_CLIENT_COMMAND:New( Client, MenuText, ParentMenu, CommandMenuFunction, ... ) - - -- Arrange meta tables - - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, MenuParentPath, CommandMenuFunction, arg ) ) -- Menu#MENU_CLIENT_COMMAND - - self.MenuClient = Client - self.MenuClientGroupID = Client:GetClientGroupID() - self.MenuParentPath = MenuParentPath - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - self:T( { Client:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText, CommandMenuFunction, arg } ) - - local MenuPathID = table.concat(MenuParentPath) .. "/" .. MenuText - if MenuPath[MenuPathID] then - missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), MenuPath[MenuPathID] ) - end - - self.MenuPath = missionCommands.addCommandForGroup( self.MenuClient:GetClientGroupID(), MenuText, MenuParentPath, self.MenuCallHandler ) - MenuPath[MenuPathID] = self.MenuPath - - if ParentMenu and ParentMenu.Menus then - ParentMenu.Menus[self.MenuPath] = self - end - - return self - end - - --- Removes a menu structure for a client. - -- @param #MENU_CLIENT_COMMAND self - -- @return #nil - function MENU_CLIENT_COMMAND:Remove() - self:F( self.MenuPath ) - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - if MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] then - MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] = nil - end - - missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), self.MenuPath ) - self.ParentMenu.Menus[self.MenuPath] = nil - return nil - end -end - ---- MENU_GROUP - -do - -- This local variable is used to cache the menus registered under groups. - -- Menus don't dissapear when groups for players are destroyed and restarted. - -- So every menu for a client created must be tracked so that program logic accidentally does not create. - -- the same menus twice during initialization logic. - -- These menu classes are handling this logic with this variable. - local _MENUGROUPS = {} - - --- @type MENU_GROUP - -- @extends Core.Menu#MENU_BASE - - - --- #MENU_GROUP class, extends @{Menu#MENU_BASE} - -- - -- The MENU_GROUP class manages the main menus for coalitions. - -- You can add menus with the @{#MENU_GROUP.New} method, which constructs a MENU_GROUP object and returns you the object reference. - -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP.Remove}. - -- - -- @usage - -- -- This demo creates a menu structure for the two groups of planes. - -- -- Each group will receive a different menu structure. - -- -- To test, join the planes, then look at the other radio menus (Option F10). - -- -- Then switch planes and check if the menu is still there. - -- -- And play with the Add and Remove menu options. - -- - -- -- Note that in multi player, this will only work after the DCS groups bug is solved. - -- - -- local function ShowStatus( PlaneGroup, StatusText, Coalition ) - -- - -- MESSAGE:New( Coalition, 15 ):ToRed() - -- PlaneGroup:Message( StatusText, 15 ) - -- end - -- - -- local MenuStatus = {} - -- - -- local function RemoveStatusMenu( MenuGroup ) - -- local MenuGroupName = MenuGroup:GetName() - -- MenuStatus[MenuGroupName]:Remove() - -- end - -- - -- --- @param Wrapper.Group#GROUP MenuGroup - -- local function AddStatusMenu( MenuGroup ) - -- local MenuGroupName = MenuGroup:GetName() - -- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object. - -- MenuStatus[MenuGroupName] = MENU_GROUP:New( MenuGroup, "Status for Planes" ) - -- MENU_GROUP_COMMAND:New( MenuGroup, "Show Status", MenuStatus[MenuGroupName], ShowStatus, MenuGroup, "Status of planes is ok!", "Message to Red Coalition" ) - -- end - -- - -- SCHEDULER:New( nil, - -- function() - -- local PlaneGroup = GROUP:FindByName( "Plane 1" ) - -- if PlaneGroup and PlaneGroup:IsAlive() then - -- local MenuManage = MENU_GROUP:New( PlaneGroup, "Manage Menus" ) - -- MENU_GROUP_COMMAND:New( PlaneGroup, "Add Status Menu Plane 1", MenuManage, AddStatusMenu, PlaneGroup ) - -- MENU_GROUP_COMMAND:New( PlaneGroup, "Remove Status Menu Plane 1", MenuManage, RemoveStatusMenu, PlaneGroup ) - -- end - -- end, {}, 10, 10 ) - -- - -- SCHEDULER:New( nil, - -- function() - -- local PlaneGroup = GROUP:FindByName( "Plane 2" ) - -- if PlaneGroup and PlaneGroup:IsAlive() then - -- local MenuManage = MENU_GROUP:New( PlaneGroup, "Manage Menus" ) - -- MENU_GROUP_COMMAND:New( PlaneGroup, "Add Status Menu Plane 2", MenuManage, AddStatusMenu, PlaneGroup ) - -- MENU_GROUP_COMMAND:New( PlaneGroup, "Remove Status Menu Plane 2", MenuManage, RemoveStatusMenu, PlaneGroup ) - -- end - -- end, {}, 10, 10 ) - -- - -- @field #MENU_GROUP - MENU_GROUP = { - ClassName = "MENU_GROUP" - } - - --- MENU_GROUP constructor. Creates a new radio menu item for a group. - -- @param #MENU_GROUP self - -- @param Wrapper.Group#GROUP MenuGroup The Group owning the menu. - -- @param #string MenuText The text for the menu. - -- @param #table ParentMenu The parent menu. - -- @return #MENU_GROUP self - function MENU_GROUP:New( MenuGroup, MenuText, ParentMenu ) - - -- Determine if the menu was not already created and already visible at the group. - -- If it is visible, then return the cached self, otherwise, create self and cache it. - - MenuGroup._Menus = MenuGroup._Menus or {} - local Path = ( ParentMenu and ( table.concat( ParentMenu.MenuPath or {}, "@" ) .. "@" .. MenuText ) ) or MenuText - if MenuGroup._Menus[Path] then - self = MenuGroup._Menus[Path] - else - self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) - --if MenuGroup:IsAlive() then - MenuGroup._Menus[Path] = self - --end - - self.MenuGroup = MenuGroup - self.Path = Path - self.MenuGroupID = MenuGroup:GetID() - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self:T( { "Adding Menu ", MenuText, self.MenuParentPath } ) - self.MenuPath = missionCommands.addSubMenuForGroup( self.MenuGroupID, MenuText, self.MenuParentPath ) - - if self.ParentMenu and self.ParentMenu.Menus then - self.ParentMenu.Menus[MenuText] = self - self:F( { self.ParentMenu.Menus, MenuText } ) - self.ParentMenu.MenuCount = self.ParentMenu.MenuCount + 1 - end - end - - --self:F( { MenuGroup:GetName(), MenuText, ParentMenu.MenuPath } ) - - return self - end - - --- Removes the sub menus recursively of this MENU_GROUP. - -- @param #MENU_GROUP self - -- @param MenuTime - -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. - -- @return #MENU_GROUP self - function MENU_GROUP:RemoveSubMenus( MenuTime, MenuTag ) - --self:F2( { self.MenuPath, MenuTime, self.MenuTime } ) - - self:T( { "Removing Group SubMenus:", MenuTime, MenuTag, self.MenuGroup:GetName(), self.MenuPath } ) - for MenuText, Menu in pairs( self.Menus ) do - Menu:Remove( MenuTime, MenuTag ) - end - - end - - - --- Removes the main menu and sub menus recursively of this MENU_GROUP. - -- @param #MENU_GROUP self - -- @param MenuTime - -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. - -- @return #nil - function MENU_GROUP:Remove( MenuTime, MenuTag ) - --self:F2( { self.MenuGroupID, self.MenuPath, MenuTime, self.MenuTime } ) - - self:RemoveSubMenus( MenuTime, MenuTag ) - - if not MenuTime or self.MenuTime ~= MenuTime then - if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then - if self.MenuGroup._Menus[self.Path] then - self = self.MenuGroup._Menus[self.Path] - - missionCommands.removeItemForGroup( self.MenuGroupID, self.MenuPath ) - if self.ParentMenu then - self.ParentMenu.Menus[self.MenuText] = nil - self.ParentMenu.MenuCount = self.ParentMenu.MenuCount - 1 - if self.ParentMenu.MenuCount == 0 then - if self.MenuRemoveParent == true then - self:T2( "Removing Parent Menu " ) - self.ParentMenu:Remove() - end - end - end - end - self:T( { "Removing Group Menu:", MenuGroup = self.MenuGroup:GetName() } ) - self.MenuGroup._Menus[self.Path] = nil - self = nil - end - end - - return nil - end - - - --- @type MENU_GROUP_COMMAND - -- @extends Core.Menu#MENU_COMMAND_BASE - - --- # MENU_GROUP_COMMAND class, extends @{Menu#MENU_COMMAND_BASE} - -- - -- The @{Menu#MENU_GROUP_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. - -- You can add menus with the @{#MENU_GROUP_COMMAND.New} method, which constructs a MENU_GROUP_COMMAND object and returns you the object reference. - -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP_COMMAND.Remove}. - -- - -- @field #MENU_GROUP_COMMAND - MENU_GROUP_COMMAND = { - ClassName = "MENU_GROUP_COMMAND" - } - - --- Creates a new radio command item for a group - -- @param #MENU_GROUP_COMMAND self - -- @param Wrapper.Group#GROUP MenuGroup The Group owning the menu. - -- @param MenuText The text for the menu. - -- @param ParentMenu The parent menu. - -- @param CommandMenuFunction A function that is called when the menu key is pressed. - -- @param CommandMenuArgument An argument for the function. - -- @return #MENU_GROUP_COMMAND - function MENU_GROUP_COMMAND:New( MenuGroup, MenuText, ParentMenu, CommandMenuFunction, ... ) - - MenuGroup._Menus = MenuGroup._Menus or {} - local Path = ( ParentMenu and ( table.concat( ParentMenu.MenuPath or {}, "@" ) .. "@" .. MenuText ) ) or MenuText - if MenuGroup._Menus[Path] then - self = MenuGroup._Menus[Path] - --self:E( { Path=Path } ) - --self:E( { self.MenuTag, self.MenuTime, "Re-using Group Command Menu:", MenuGroup:GetName(), MenuText } ) - self:SetCommandMenuFunction( CommandMenuFunction ) - self:SetCommandMenuArguments( arg ) - return self - end - self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) - - --if MenuGroup:IsAlive() then - MenuGroup._Menus[Path] = self - --end - - --self:E({Path=Path}) - self.Path = Path - self.MenuGroup = MenuGroup - self.MenuGroupID = MenuGroup:GetID() - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self:F( { "Adding Group Command Menu:", MenuGroup = MenuGroup:GetName(), MenuText = MenuText, MenuPath = self.MenuParentPath } ) - self.MenuPath = missionCommands.addCommandForGroup( self.MenuGroupID, MenuText, self.MenuParentPath, self.MenuCallHandler ) - - if self.ParentMenu and self.ParentMenu.Menus then - self.ParentMenu.Menus[MenuText] = self - self.ParentMenu.MenuCount = self.ParentMenu.MenuCount + 1 - self:F2( { ParentMenu.Menus, MenuText } ) - end --- end - - return self - end - - --- Removes a menu structure for a group. - -- @param #MENU_GROUP_COMMAND self - -- @param MenuTime - -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. - -- @return #nil - function MENU_GROUP_COMMAND:Remove( MenuTime, MenuTag ) - --self:F2( { self.MenuGroupID, self.MenuPath, MenuTime, self.MenuTime } ) - - --self:E( { MenuTag = MenuTag, MenuTime = self.MenuTime, Path = self.Path } ) - if not MenuTime or self.MenuTime ~= MenuTime then - if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then - if self.MenuGroup._Menus[self.Path] then - self = self.MenuGroup._Menus[self.Path] - - missionCommands.removeItemForGroup( self.MenuGroupID, self.MenuPath ) - --self:E( { "Removing Group Command Menu:", MenuGroup = self.MenuGroup:GetName(), MenuText = self.MenuText, MenuPath = self.Path } ) - - self.ParentMenu.Menus[self.MenuText] = nil - self.ParentMenu.MenuCount = self.ParentMenu.MenuCount - 1 - if self.ParentMenu.MenuCount == 0 then - if self.MenuRemoveParent == true then - self:T2( "Removing Parent Menu " ) - self.ParentMenu:Remove() - end - end - - self.MenuGroup._Menus[self.Path] = nil - self = nil - end - end - end - - return nil - end - -end - ---- **Core** -- ZONE classes define **zones** within your mission of **various forms**, with **various capabilities**. --- --- ![Banner Image](..\Presentations\ZONE\Dia1.JPG) --- --- ==== --- --- There are essentially two core functions that zones accomodate: --- --- * Test if an object is within the zone boundaries. --- * Provide the zone behaviour. Some zones are static, while others are moveable. --- --- The object classes are using the zone classes to test the zone boundaries, which can take various forms: --- --- * Test if completely within the zone. --- * Test if partly within the zone (for @{Group#GROUP} objects). --- * Test if not in the zone. --- * Distance to the nearest intersecting point of the zone. --- * Distance to the center of the zone. --- * ... --- --- Each of these ZONE classes have a zone name, and specific parameters defining the zone type: --- --- * @{#ZONE_BASE}: The ZONE_BASE class defining the base for all other zone classes. --- * @{#ZONE_RADIUS}: The ZONE_RADIUS class defined by a zone name, a location and a radius. --- * @{#ZONE}: The ZONE class, defined by the zone name as defined within the Mission Editor. --- * @{#ZONE_UNIT}: The ZONE_UNIT class defines by a zone around a @{Unit#UNIT} with a radius. --- * @{#ZONE_GROUP}: The ZONE_GROUP class defines by a zone around a @{Group#GROUP} with a radius. --- * @{#ZONE_POLYGON}: The ZONE_POLYGON class defines by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- ### Contributions: --- --- ==== --- --- @module Zone - - ---- @type ZONE_BASE --- @field #string ZoneName Name of the zone. --- @field #number ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. --- @extends Core.Base#BASE - - ---- # ZONE_BASE class, extends @{Base#BASE} --- --- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. --- --- ## Each zone has a name: --- --- * @{#ZONE_BASE.GetName}(): Returns the name of the zone. --- --- ## Each zone implements two polymorphic functions defined in @{Zone#ZONE_BASE}: --- --- * @{#ZONE_BASE.IsVec2InZone}(): Returns if a 2D vector is within the zone. --- * @{#ZONE_BASE.IsVec3InZone}(): Returns if a 3D vector is within the zone. --- * @{#ZONE_BASE.IsPointVec2InZone}(): Returns if a 2D point vector is within the zone. --- * @{#ZONE_BASE.IsPointVec3InZone}(): Returns if a 3D point vector is within the zone. --- --- ## A zone has a probability factor that can be set to randomize a selection between zones: --- --- * @{#ZONE_BASE.SetZoneProbability}(): Set the randomization probability of a zone to be selected, taking a value between 0 and 1 ( 0 = 0%, 1 = 100% ) --- * @{#ZONE_BASE.GetZoneProbability}(): Get the randomization probability of a zone to be selected, passing a value between 0 and 1 ( 0 = 0%, 1 = 100% ) --- * @{#ZONE_BASE.GetZoneMaybe}(): Get the zone taking into account the randomization probability. nil is returned if this zone is not a candidate. --- --- ## A zone manages vectors: --- --- * @{#ZONE_BASE.GetVec2}(): Returns the 2D vector coordinate of the zone. --- * @{#ZONE_BASE.GetVec3}(): Returns the 3D vector coordinate of the zone. --- * @{#ZONE_BASE.GetPointVec2}(): Returns the 2D point vector coordinate of the zone. --- * @{#ZONE_BASE.GetPointVec3}(): Returns the 3D point vector coordinate of the zone. --- * @{#ZONE_BASE.GetRandomVec2}(): Define a random 2D vector within the zone. --- * @{#ZONE_BASE.GetRandomPointVec2}(): Define a random 2D point vector within the zone. --- * @{#ZONE_BASE.GetRandomPointVec3}(): Define a random 3D point vector within the zone. --- --- ## A zone has a bounding square: --- --- * @{#ZONE_BASE.GetBoundingSquare}(): Get the outer most bounding square of the zone. --- --- ## A zone can be marked: --- --- * @{#ZONE_BASE.SmokeZone}(): Smokes the zone boundaries in a color. --- * @{#ZONE_BASE.FlareZone}(): Flares the zone boundaries in a color. --- --- @field #ZONE_BASE -ZONE_BASE = { - ClassName = "ZONE_BASE", - ZoneName = "", - ZoneProbability = 1, - } - - ---- The ZONE_BASE.BoundingSquare --- @type ZONE_BASE.BoundingSquare --- @field Dcs.DCSTypes#Distance x1 The lower x coordinate (left down) --- @field Dcs.DCSTypes#Distance y1 The lower y coordinate (left down) --- @field Dcs.DCSTypes#Distance x2 The higher x coordinate (right up) --- @field Dcs.DCSTypes#Distance y2 The higher y coordinate (right up) - - ---- ZONE_BASE constructor --- @param #ZONE_BASE self --- @param #string ZoneName Name of the zone. --- @return #ZONE_BASE self -function ZONE_BASE:New( ZoneName ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( ZoneName ) - - self.ZoneName = ZoneName - - return self -end - ---- Returns the name of the zone. --- @param #ZONE_BASE self --- @return #string The name of the zone. -function ZONE_BASE:GetName() - self:F2() - - return self.ZoneName -end - ---- Returns if a Vec2 is within the zone. --- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 to test. --- @return #boolean true if the Vec2 is within the zone. -function ZONE_BASE:IsVec2InZone( Vec2 ) - self:F2( Vec2 ) - - return false -end - ---- Returns if a Vec3 is within the zone. --- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Vec3 Vec3 The point to test. --- @return #boolean true if the Vec3 is within the zone. -function ZONE_BASE:IsVec3InZone( Vec3 ) - self:F2( Vec3 ) - - local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) - - return InZone -end - ---- Returns if a PointVec2 is within the zone. --- @param #ZONE_BASE self --- @param Core.Point#POINT_VEC2 PointVec2 The PointVec2 to test. --- @return #boolean true if the PointVec2 is within the zone. -function ZONE_BASE:IsPointVec2InZone( PointVec2 ) - self:F2( PointVec2 ) - - local InZone = self:IsVec2InZone( PointVec2:GetVec2() ) - - return InZone -end - ---- Returns if a PointVec3 is within the zone. --- @param #ZONE_BASE self --- @param Core.Point#POINT_VEC3 PointVec3 The PointVec3 to test. --- @return #boolean true if the PointVec3 is within the zone. -function ZONE_BASE:IsPointVec3InZone( PointVec3 ) - self:F2( PointVec3 ) - - local InZone = self:IsPointVec2InZone( PointVec3 ) - - return InZone -end - - ---- Returns the @{DCSTypes#Vec2} coordinate of the zone. --- @param #ZONE_BASE self --- @return #nil. -function ZONE_BASE:GetVec2() - self:F2( self.ZoneName ) - - return nil -end - ---- Returns a @{Point#POINT_VEC2} of the zone. --- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Core.Point#POINT_VEC2 The PointVec2 of the zone. -function ZONE_BASE:GetPointVec2() - self:F2( self.ZoneName ) - - local Vec2 = self:GetVec2() - - local PointVec2 = POINT_VEC2:NewFromVec2( Vec2 ) - - self:T2( { PointVec2 } ) - - return PointVec2 -end - - ---- Returns a @{Point#COORDINATE} of the zone. --- @param #ZONE_BASE self --- @return Core.Point#COORDINATE The Coordinate of the zone. -function ZONE_BASE:GetCoordinate() - self:F2( self.ZoneName ) - - local Vec2 = self:GetVec2() - - local Coordinate = COORDINATE:NewFromVec2( Vec2 ) - - self:T2( { Coordinate } ) - - return Coordinate -end - - ---- Returns the @{DCSTypes#Vec3} of the zone. --- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Dcs.DCSTypes#Vec3 The Vec3 of the zone. -function ZONE_BASE:GetVec3( Height ) - self:F2( self.ZoneName ) - - Height = Height or 0 - - local Vec2 = self:GetVec2() - - local Vec3 = { x = Vec2.x, y = Height and Height or land.getHeight( self:GetVec2() ), z = Vec2.y } - - self:T2( { Vec3 } ) - - return Vec3 -end - ---- Returns a @{Point#POINT_VEC3} of the zone. --- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Core.Point#POINT_VEC3 The PointVec3 of the zone. -function ZONE_BASE:GetPointVec3( Height ) - self:F2( self.ZoneName ) - - local Vec3 = self:GetVec3( Height ) - - local PointVec3 = POINT_VEC3:NewFromVec3( Vec3 ) - - self:T2( { PointVec3 } ) - - return PointVec3 -end - ---- Returns a @{Point#COORDINATE} of the zone. --- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Core.Point#COORDINATE The Coordinate of the zone. -function ZONE_BASE:GetCoordinate( Height ) --R2.1 - self:F2( self.ZoneName ) - - local Vec3 = self:GetVec3( Height ) - - local PointVec3 = COORDINATE:NewFromVec3( Vec3 ) - - self:T2( { PointVec3 } ) - - return PointVec3 -end - - ---- Define a random @{DCSTypes#Vec2} within the zone. --- @param #ZONE_BASE self --- @return Dcs.DCSTypes#Vec2 The Vec2 coordinates. -function ZONE_BASE:GetRandomVec2() - return nil -end - ---- Define a random @{Point#POINT_VEC2} within the zone. --- @param #ZONE_BASE self --- @return Core.Point#POINT_VEC2 The PointVec2 coordinates. -function ZONE_BASE:GetRandomPointVec2() - return nil -end - ---- Define a random @{Point#POINT_VEC3} within the zone. --- @param #ZONE_BASE self --- @return Core.Point#POINT_VEC3 The PointVec3 coordinates. -function ZONE_BASE:GetRandomPointVec3() - return nil -end - ---- Get the bounding square the zone. --- @param #ZONE_BASE self --- @return #nil The bounding square. -function ZONE_BASE:GetBoundingSquare() - --return { x1 = 0, y1 = 0, x2 = 0, y2 = 0 } - return nil -end - ---- Bound the zone boundaries with a tires. --- @param #ZONE_BASE self -function ZONE_BASE:BoundZone() - self:F2() - -end - ---- Smokes the zone boundaries in a color. --- @param #ZONE_BASE self --- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. -function ZONE_BASE:SmokeZone( SmokeColor ) - self:F2( SmokeColor ) - -end - ---- Set the randomization probability of a zone to be selected. --- @param #ZONE_BASE self --- @param ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. -function ZONE_BASE:SetZoneProbability( ZoneProbability ) - self:F2( ZoneProbability ) - - self.ZoneProbability = ZoneProbability or 1 - return self -end - ---- Get the randomization probability of a zone to be selected. --- @param #ZONE_BASE self --- @return #number A value between 0 and 1. 0 = 0% and 1 = 100% probability. -function ZONE_BASE:GetZoneProbability() - self:F2() - - return self.ZoneProbability -end - ---- Get the zone taking into account the randomization probability of a zone to be selected. --- @param #ZONE_BASE self --- @return #ZONE_BASE The zone is selected taking into account the randomization probability factor. --- @return #nil The zone is not selected taking into account the randomization probability factor. -function ZONE_BASE:GetZoneMaybe() - self:F2() - - local Randomization = math.random() - if Randomization <= self.ZoneProbability then - return self - else - return nil - end -end - - ---- The ZONE_RADIUS class, defined by a zone name, a location and a radius. --- @type ZONE_RADIUS --- @field Dcs.DCSTypes#Vec2 Vec2 The current location of the zone. --- @field Dcs.DCSTypes#Distance Radius The radius of the zone. --- @extends #ZONE_BASE - ---- # ZONE_RADIUS class, extends @{Zone#ZONE_BASE} --- --- The ZONE_RADIUS class defined by a zone name, a location and a radius. --- This class implements the inherited functions from Core.Zone#ZONE_BASE taking into account the own zone format and properties. --- --- ## ZONE_RADIUS constructor --- --- * @{#ZONE_RADIUS.New}(): Constructor. --- --- ## Manage the radius of the zone --- --- * @{#ZONE_RADIUS.SetRadius}(): Sets the radius of the zone. --- * @{#ZONE_RADIUS.GetRadius}(): Returns the radius of the zone. --- --- ## Manage the location of the zone --- --- * @{#ZONE_RADIUS.SetVec2}(): Sets the @{DCSTypes#Vec2} of the zone. --- * @{#ZONE_RADIUS.GetVec2}(): Returns the @{DCSTypes#Vec2} of the zone. --- * @{#ZONE_RADIUS.GetVec3}(): Returns the @{DCSTypes#Vec3} of the zone, taking an additional height parameter. --- --- ## Zone point randomization --- --- Various functions exist to find random points within the zone. --- --- * @{#ZONE_RADIUS.GetRandomVec2}(): Gets a random 2D point in the zone. --- * @{#ZONE_RADIUS.GetRandomPointVec2}(): Gets a @{Point#POINT_VEC2} object representing a random 2D point in the zone. --- * @{#ZONE_RADIUS.GetRandomPointVec3}(): Gets a @{Point#POINT_VEC3} object representing a random 3D point in the zone. Note that the height of the point is at landheight. --- --- @field #ZONE_RADIUS -ZONE_RADIUS = { - ClassName="ZONE_RADIUS", - } - ---- Constructor of @{#ZONE_RADIUS}, taking the zone name, the zone location and a radius. --- @param #ZONE_RADIUS self --- @param #string ZoneName Name of the zone. --- @param Dcs.DCSTypes#Vec2 Vec2 The location of the zone. --- @param Dcs.DCSTypes#Distance Radius The radius of the zone. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:New( ZoneName, Vec2, Radius ) - local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) -- #ZONE_RADIUS - self:F( { ZoneName, Vec2, Radius } ) - - self.Radius = Radius - self.Vec2 = Vec2 - - return self -end - ---- Bounds the zone with tires. --- @param #ZONE_RADIUS self --- @param #number Points (optional) The amount of points in the circle. --- @param #boolean UnBound If true the tyres will be destroyed. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:BoundZone( Points, CountryID, UnBound ) - - local Point = {} - local Vec2 = self:GetVec2() - - Points = Points and Points or 360 - - local Angle - local RadialBase = math.pi*2 - - -- - for Angle = 0, 360, (360 / Points ) do - local Radial = Angle * RadialBase / 360 - Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() - Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() - - local CountryName = _DATABASE.COUNTRY_NAME[CountryID] - - local Tire = { - ["country"] = CountryName, - ["category"] = "Fortifications", - ["canCargo"] = false, - ["shape_name"] = "H-tyre_B_WF", - ["type"] = "Black_Tyre_WF", - --["unitId"] = Angle + 10000, - ["y"] = Point.y, - ["x"] = Point.x, - ["name"] = string.format( "%s-Tire #%0d", self:GetName(), Angle ), - ["heading"] = 0, - } -- end of ["group"] - - local Group = coalition.addStaticObject( CountryID, Tire ) - if UnBound and UnBound == true then - Group:destroy() - end - end - - return self -end - - ---- Smokes the zone boundaries in a color. --- @param #ZONE_RADIUS self --- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. --- @param #number Points (optional) The amount of points in the circle. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:SmokeZone( SmokeColor, Points ) - self:F2( SmokeColor ) - - local Point = {} - local Vec2 = self:GetVec2() - - Points = Points and Points or 360 - - local Angle - local RadialBase = math.pi*2 - - for Angle = 0, 360, 360 / Points do - local Radial = Angle * RadialBase / 360 - Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() - Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() - POINT_VEC2:New( Point.x, Point.y ):Smoke( SmokeColor ) - end - - return self -end - - ---- Flares the zone boundaries in a color. --- @param #ZONE_RADIUS self --- @param Utilities.Utils#FLARECOLOR FlareColor The flare color. --- @param #number Points (optional) The amount of points in the circle. --- @param Dcs.DCSTypes#Azimuth Azimuth (optional) Azimuth The azimuth of the flare. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:FlareZone( FlareColor, Points, Azimuth ) - self:F2( { FlareColor, Azimuth } ) - - local Point = {} - local Vec2 = self:GetVec2() - - Points = Points and Points or 360 - - local Angle - local RadialBase = math.pi*2 - - for Angle = 0, 360, 360 / Points do - local Radial = Angle * RadialBase / 360 - Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() - Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() - POINT_VEC2:New( Point.x, Point.y ):Flare( FlareColor, Azimuth ) - end - - return self -end - ---- Returns the radius of the zone. --- @param #ZONE_RADIUS self --- @return Dcs.DCSTypes#Distance The radius of the zone. -function ZONE_RADIUS:GetRadius() - self:F2( self.ZoneName ) - - self:T2( { self.Radius } ) - - return self.Radius -end - ---- Sets the radius of the zone. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Distance Radius The radius of the zone. --- @return Dcs.DCSTypes#Distance The radius of the zone. -function ZONE_RADIUS:SetRadius( Radius ) - self:F2( self.ZoneName ) - - self.Radius = Radius - self:T2( { self.Radius } ) - - return self.Radius -end - ---- Returns the @{DCSTypes#Vec2} of the zone. --- @param #ZONE_RADIUS self --- @return Dcs.DCSTypes#Vec2 The location of the zone. -function ZONE_RADIUS:GetVec2() - self:F2( self.ZoneName ) - - self:T2( { self.Vec2 } ) - - return self.Vec2 -end - ---- Sets the @{DCSTypes#Vec2} of the zone. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Vec2 Vec2 The new location of the zone. --- @return Dcs.DCSTypes#Vec2 The new location of the zone. -function ZONE_RADIUS:SetVec2( Vec2 ) - self:F2( self.ZoneName ) - - self.Vec2 = Vec2 - - self:T2( { self.Vec2 } ) - - return self.Vec2 -end - ---- Returns the @{DCSTypes#Vec3} of the ZONE_RADIUS. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Dcs.DCSTypes#Vec3 The point of the zone. -function ZONE_RADIUS:GetVec3( Height ) - self:F2( { self.ZoneName, Height } ) - - Height = Height or 0 - local Vec2 = self:GetVec2() - - local Vec3 = { x = Vec2.x, y = land.getHeight( self:GetVec2() ) + Height, z = Vec2.y } - - self:T2( { Vec3 } ) - - return Vec3 -end - - ---- Returns if a location is within the zone. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. --- @return #boolean true if the location is within the zone. -function ZONE_RADIUS:IsVec2InZone( Vec2 ) - self:F2( Vec2 ) - - local ZoneVec2 = self:GetVec2() - - if ZoneVec2 then - if (( Vec2.x - ZoneVec2.x )^2 + ( Vec2.y - ZoneVec2.y ) ^2 ) ^ 0.5 <= self:GetRadius() then - return true - end - end - - return false -end - ---- Returns if a point is within the zone. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Vec3 Vec3 The point to test. --- @return #boolean true if the point is within the zone. -function ZONE_RADIUS:IsVec3InZone( Vec3 ) - self:F2( Vec3 ) - - local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) - - return InZone -end - ---- Returns a random Vec2 location within the zone. --- @param #ZONE_RADIUS self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Dcs.DCSTypes#Vec2 The random location within the zone. -function ZONE_RADIUS:GetRandomVec2( inner, outer ) - self:F( self.ZoneName, inner, outer ) - - local Point = {} - local Vec2 = self:GetVec2() - local _inner = inner or 0 - local _outer = outer or self:GetRadius() - - local angle = math.random() * math.pi * 2; - Point.x = Vec2.x + math.cos( angle ) * math.random(_inner, _outer); - Point.y = Vec2.y + math.sin( angle ) * math.random(_inner, _outer); - - self:T( { Point } ) - - return Point -end - ---- Returns a @{Point#POINT_VEC2} object reflecting a random 2D location within the zone. --- @param #ZONE_RADIUS self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Core.Point#POINT_VEC2 The @{Point#POINT_VEC2} object reflecting the random 3D location within the zone. -function ZONE_RADIUS:GetRandomPointVec2( inner, outer ) - self:F( self.ZoneName, inner, outer ) - - local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) - - self:T3( { PointVec2 } ) - - return PointVec2 -end - ---- Returns a @{Point#POINT_VEC3} object reflecting a random 3D location within the zone. --- @param #ZONE_RADIUS self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Core.Point#POINT_VEC3 The @{Point#POINT_VEC3} object reflecting the random 3D location within the zone. -function ZONE_RADIUS:GetRandomPointVec3( inner, outer ) - self:F( self.ZoneName, inner, outer ) - - local PointVec3 = POINT_VEC3:NewFromVec2( self:GetRandomVec2() ) - - self:T3( { PointVec3 } ) - - return PointVec3 -end - - ---- Returns a @{Point#COORDINATE} object reflecting a random 3D location within the zone. --- @param #ZONE_RADIUS self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Core.Point#COORDINATE -function ZONE_RADIUS:GetRandomCoordinate( inner, outer ) - self:F( self.ZoneName, inner, outer ) - - local Coordinate = COORDINATE:NewFromVec2( self:GetRandomVec2() ) - - self:T3( { Coordinate = Coordinate } ) - - return Coordinate -end - - - ---- @type ZONE --- @extends #ZONE_RADIUS - - ---- # ZONE class, extends @{Zone#ZONE_RADIUS} --- --- The ZONE class, defined by the zone name as defined within the Mission Editor. --- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties. --- --- @field #ZONE -ZONE = { - ClassName="ZONE", - } - - ---- Constructor of ZONE, taking the zone name. --- @param #ZONE self --- @param #string ZoneName The name of the zone as defined within the mission editor. --- @return #ZONE -function ZONE:New( ZoneName ) - - local Zone = trigger.misc.getZone( ZoneName ) - - if not Zone then - error( "Zone " .. ZoneName .. " does not exist." ) - return nil - end - - local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, { x = Zone.point.x, y = Zone.point.z }, Zone.radius ) ) - self:F( ZoneName ) - - self.Zone = Zone - - return self -end - - ---- @type ZONE_UNIT --- @field Wrapper.Unit#UNIT ZoneUNIT --- @extends Core.Zone#ZONE_RADIUS - ---- # ZONE_UNIT class, extends @{Zone#ZONE_RADIUS} --- --- The ZONE_UNIT class defined by a zone around a @{Unit#UNIT} with a radius. --- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties. --- --- @field #ZONE_UNIT -ZONE_UNIT = { - ClassName="ZONE_UNIT", - } - ---- Constructor to create a ZONE_UNIT instance, taking the zone name, a zone unit and a radius. --- @param #ZONE_UNIT self --- @param #string ZoneName Name of the zone. --- @param Wrapper.Unit#UNIT ZoneUNIT The unit as the center of the zone. --- @param Dcs.DCSTypes#Distance Radius The radius of the zone. --- @return #ZONE_UNIT self -function ZONE_UNIT:New( ZoneName, ZoneUNIT, Radius ) - local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneUNIT:GetVec2(), Radius ) ) - self:F( { ZoneName, ZoneUNIT:GetVec2(), Radius } ) - - self.ZoneUNIT = ZoneUNIT - self.LastVec2 = ZoneUNIT:GetVec2() - - return self -end - - ---- Returns the current location of the @{Unit#UNIT}. --- @param #ZONE_UNIT self --- @return Dcs.DCSTypes#Vec2 The location of the zone based on the @{Unit#UNIT}location. -function ZONE_UNIT:GetVec2() - self:F2( self.ZoneName ) - - local ZoneVec2 = self.ZoneUNIT:GetVec2() - if ZoneVec2 then - self.LastVec2 = ZoneVec2 - return ZoneVec2 - else - return self.LastVec2 - end - - self:T2( { ZoneVec2 } ) - - return nil -end - ---- Returns a random location within the zone. --- @param #ZONE_UNIT self --- @return Dcs.DCSTypes#Vec2 The random location within the zone. -function ZONE_UNIT:GetRandomVec2() - self:F( self.ZoneName ) - - local RandomVec2 = {} - local Vec2 = self.ZoneUNIT:GetVec2() - - if not Vec2 then - Vec2 = self.LastVec2 - end - - local angle = math.random() * math.pi*2; - RandomVec2.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); - RandomVec2.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); - - self:T( { RandomVec2 } ) - - return RandomVec2 -end - ---- Returns the @{DCSTypes#Vec3} of the ZONE_UNIT. --- @param #ZONE_UNIT self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Dcs.DCSTypes#Vec3 The point of the zone. -function ZONE_UNIT:GetVec3( Height ) - self:F2( self.ZoneName ) - - Height = Height or 0 - - local Vec2 = self:GetVec2() - - local Vec3 = { x = Vec2.x, y = land.getHeight( self:GetVec2() ) + Height, z = Vec2.y } - - self:T2( { Vec3 } ) - - return Vec3 -end - ---- @type ZONE_GROUP --- @extends #ZONE_RADIUS - - ---- # ZONE_GROUP class, extends @{Zone#ZONE_RADIUS} --- --- The ZONE_GROUP class defines by a zone around a @{Group#GROUP} with a radius. The current leader of the group defines the center of the zone. --- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- --- @field #ZONE_GROUP -ZONE_GROUP = { - ClassName="ZONE_GROUP", - } - ---- Constructor to create a ZONE_GROUP instance, taking the zone name, a zone @{Group#GROUP} and a radius. --- @param #ZONE_GROUP self --- @param #string ZoneName Name of the zone. --- @param Wrapper.Group#GROUP ZoneGROUP The @{Group} as the center of the zone. --- @param Dcs.DCSTypes#Distance Radius The radius of the zone. --- @return #ZONE_GROUP self -function ZONE_GROUP:New( ZoneName, ZoneGROUP, Radius ) - local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneGROUP:GetVec2(), Radius ) ) - self:F( { ZoneName, ZoneGROUP:GetVec2(), Radius } ) - - self._.ZoneGROUP = ZoneGROUP - - return self -end - - ---- Returns the current location of the @{Group}. --- @param #ZONE_GROUP self --- @return Dcs.DCSTypes#Vec2 The location of the zone based on the @{Group} location. -function ZONE_GROUP:GetVec2() - self:F( self.ZoneName ) - - local ZoneVec2 = self._.ZoneGROUP:GetVec2() - - self:T( { ZoneVec2 } ) - - return ZoneVec2 -end - ---- Returns a random location within the zone of the @{Group}. --- @param #ZONE_GROUP self --- @return Dcs.DCSTypes#Vec2 The random location of the zone based on the @{Group} location. -function ZONE_GROUP:GetRandomVec2() - self:F( self.ZoneName ) - - local Point = {} - local Vec2 = self._.ZoneGROUP:GetVec2() - - local angle = math.random() * math.pi*2; - Point.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); - Point.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); - - self:T( { Point } ) - - return Point -end - ---- Returns a @{Point#POINT_VEC2} object reflecting a random 2D location within the zone. --- @param #ZONE_GROUP self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Core.Point#POINT_VEC2 The @{Point#POINT_VEC2} object reflecting the random 3D location within the zone. -function ZONE_GROUP:GetRandomPointVec2( inner, outer ) - self:F( self.ZoneName, inner, outer ) - - local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) - - self:T3( { PointVec2 } ) - - return PointVec2 -end - - ---- @type ZONE_POLYGON_BASE --- --@field #ZONE_POLYGON_BASE.ListVec2 Polygon The polygon defined by an array of @{DCSTypes#Vec2}. --- @extends #ZONE_BASE - - ---- # ZONE_POLYGON_BASE class, extends @{Zone#ZONE_BASE} --- --- The ZONE_POLYGON_BASE class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. --- --- ## Zone point randomization --- --- Various functions exist to find random points within the zone. --- --- * @{#ZONE_POLYGON_BASE.GetRandomVec2}(): Gets a random 2D point in the zone. --- * @{#ZONE_POLYGON_BASE.GetRandomPointVec2}(): Return a @{Point#POINT_VEC2} object representing a random 2D point within the zone. --- * @{#ZONE_POLYGON_BASE.GetRandomPointVec3}(): Return a @{Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. --- --- @field #ZONE_POLYGON_BASE -ZONE_POLYGON_BASE = { - ClassName="ZONE_POLYGON_BASE", - } - ---- A points array. --- @type ZONE_POLYGON_BASE.ListVec2 --- @list - ---- Constructor to create a ZONE_POLYGON_BASE instance, taking the zone name and an array of @{DCSTypes#Vec2}, forming a polygon. --- The @{Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected. --- @param #ZONE_POLYGON_BASE self --- @param #string ZoneName Name of the zone. --- @param #ZONE_POLYGON_BASE.ListVec2 PointsArray An array of @{DCSTypes#Vec2}, forming a polygon.. --- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:New( ZoneName, PointsArray ) - local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) - self:F( { ZoneName, PointsArray } ) - - local i = 0 - - self._.Polygon = {} - - for i = 1, #PointsArray do - self._.Polygon[i] = {} - self._.Polygon[i].x = PointsArray[i].x - self._.Polygon[i].y = PointsArray[i].y - end - - return self -end - ---- Returns the center location of the polygon. --- @param #ZONE_GROUP self --- @return Dcs.DCSTypes#Vec2 The location of the zone based on the @{Group} location. -function ZONE_POLYGON_BASE:GetVec2() - self:F( self.ZoneName ) - - local Bounds = self:GetBoundingSquare() - - return { x = ( Bounds.x2 + Bounds.x1 ) / 2, y = ( Bounds.y2 + Bounds.y1 ) / 2 } -end - ---- Flush polygon coordinates as a table in DCS.log. --- @param #ZONE_POLYGON_BASE self --- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:Flush() - self:F2() - - self:E( { Polygon = self.ZoneName, Coordinates = self._.Polygon } ) - - return self -end - ---- Smokes the zone boundaries in a color. --- @param #ZONE_POLYGON_BASE self --- @param #boolean UnBound If true, the tyres will be destroyed. --- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:BoundZone( UnBound ) - - local i - local j - local Segments = 10 - - i = 1 - j = #self._.Polygon - - while i <= #self._.Polygon do - self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) - - local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x - local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y - - for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. - local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) - local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) - local Tire = { - ["country"] = "USA", - ["category"] = "Fortifications", - ["canCargo"] = false, - ["shape_name"] = "H-tyre_B_WF", - ["type"] = "Black_Tyre_WF", - ["y"] = PointY, - ["x"] = PointX, - ["name"] = string.format( "%s-Tire #%0d", self:GetName(), ((i - 1) * Segments) + Segment ), - ["heading"] = 0, - } -- end of ["group"] - - local Group = coalition.addStaticObject( country.id.USA, Tire ) - if UnBound and UnBound == true then - Group:destroy() - end - - end - j = i - i = i + 1 - end - - return self -end - - - ---- Smokes the zone boundaries in a color. --- @param #ZONE_POLYGON_BASE self --- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. --- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:SmokeZone( SmokeColor ) - self:F2( SmokeColor ) - - local i - local j - local Segments = 10 - - i = 1 - j = #self._.Polygon - - while i <= #self._.Polygon do - self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) - - local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x - local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y - - for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. - local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) - local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) - POINT_VEC2:New( PointX, PointY ):Smoke( SmokeColor ) - end - j = i - i = i + 1 - end - - return self -end - - - - ---- Returns if a location is within the zone. --- Source learned and taken from: https://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html --- @param #ZONE_POLYGON_BASE self --- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. --- @return #boolean true if the location is within the zone. -function ZONE_POLYGON_BASE:IsVec2InZone( Vec2 ) - self:F2( Vec2 ) - - local Next - local Prev - local InPolygon = false - - Next = 1 - Prev = #self._.Polygon - - while Next <= #self._.Polygon do - self:T( { Next, Prev, self._.Polygon[Next], self._.Polygon[Prev] } ) - if ( ( ( self._.Polygon[Next].y > Vec2.y ) ~= ( self._.Polygon[Prev].y > Vec2.y ) ) and - ( Vec2.x < ( self._.Polygon[Prev].x - self._.Polygon[Next].x ) * ( Vec2.y - self._.Polygon[Next].y ) / ( self._.Polygon[Prev].y - self._.Polygon[Next].y ) + self._.Polygon[Next].x ) - ) then - InPolygon = not InPolygon - end - self:T2( { InPolygon = InPolygon } ) - Prev = Next - Next = Next + 1 - end - - self:T( { InPolygon = InPolygon } ) - return InPolygon -end - ---- Define a random @{DCSTypes#Vec2} within the zone. --- @param #ZONE_POLYGON_BASE self --- @return Dcs.DCSTypes#Vec2 The Vec2 coordinate. -function ZONE_POLYGON_BASE:GetRandomVec2() - self:F2() - - --- It is a bit tricky to find a random point within a polygon. Right now i am doing it the dirty and inefficient way... - local Vec2Found = false - local Vec2 - local BS = self:GetBoundingSquare() - - self:T2( BS ) - - while Vec2Found == false do - Vec2 = { x = math.random( BS.x1, BS.x2 ), y = math.random( BS.y1, BS.y2 ) } - self:T2( Vec2 ) - if self:IsVec2InZone( Vec2 ) then - Vec2Found = true - end - end - - self:T2( Vec2 ) - - return Vec2 -end - ---- Return a @{Point#POINT_VEC2} object representing a random 2D point at landheight within the zone. --- @param #ZONE_POLYGON_BASE self --- @return @{Point#POINT_VEC2} -function ZONE_POLYGON_BASE:GetRandomPointVec2() - self:F2() - - local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) - - self:T2( PointVec2 ) - - return PointVec2 -end - ---- Return a @{Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. --- @param #ZONE_POLYGON_BASE self --- @return @{Point#POINT_VEC3} -function ZONE_POLYGON_BASE:GetRandomPointVec3() - self:F2() - - local PointVec3 = POINT_VEC3:NewFromVec2( self:GetRandomVec2() ) - - self:T2( PointVec3 ) - - return PointVec3 -end - - ---- Return a @{Point#COORDINATE} object representing a random 3D point at landheight within the zone. --- @param #ZONE_POLYGON_BASE self --- @return Core.Point#COORDINATE -function ZONE_POLYGON_BASE:GetRandomCoordinate() - self:F2() - - local Coordinate = COORDINATE:NewFromVec2( self:GetRandomVec2() ) - - self:T2( Coordinate ) - - return Coordinate -end - - ---- Get the bounding square the zone. --- @param #ZONE_POLYGON_BASE self --- @return #ZONE_POLYGON_BASE.BoundingSquare The bounding square. -function ZONE_POLYGON_BASE:GetBoundingSquare() - - local x1 = self._.Polygon[1].x - local y1 = self._.Polygon[1].y - local x2 = self._.Polygon[1].x - local y2 = self._.Polygon[1].y - - for i = 2, #self._.Polygon do - self:T2( { self._.Polygon[i], x1, y1, x2, y2 } ) - x1 = ( x1 > self._.Polygon[i].x ) and self._.Polygon[i].x or x1 - x2 = ( x2 < self._.Polygon[i].x ) and self._.Polygon[i].x or x2 - y1 = ( y1 > self._.Polygon[i].y ) and self._.Polygon[i].y or y1 - y2 = ( y2 < self._.Polygon[i].y ) and self._.Polygon[i].y or y2 - - end - - return { x1 = x1, y1 = y1, x2 = x2, y2 = y2 } -end - - ---- @type ZONE_POLYGON --- @extends #ZONE_POLYGON_BASE - - ---- # ZONE_POLYGON class, extends @{Zone#ZONE_POLYGON_BASE} --- --- The ZONE_POLYGON class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- --- @field #ZONE_POLYGON -ZONE_POLYGON = { - ClassName="ZONE_POLYGON", - } - ---- Constructor to create a ZONE_POLYGON instance, taking the zone name and the name of the @{Group#GROUP} defined within the Mission Editor. --- The @{Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected by ZONE_POLYGON. --- @param #ZONE_POLYGON self --- @param #string ZoneName Name of the zone. --- @param Wrapper.Group#GROUP ZoneGroup The GROUP waypoints as defined within the Mission Editor define the polygon shape. --- @return #ZONE_POLYGON self -function ZONE_POLYGON:New( ZoneName, ZoneGroup ) - - local GroupPoints = ZoneGroup:GetTaskRoute() - - local self = BASE:Inherit( self, ZONE_POLYGON_BASE:New( ZoneName, GroupPoints ) ) - self:F( { ZoneName, ZoneGroup, self._.Polygon } ) - - return self -end - ---- **Core** -- DATABASE manages the database of mission objects. --- --- ==== --- --- 1) @{#DATABASE} class, extends @{Base#BASE} --- =================================================== --- Mission designers can use the DATABASE class to refer to: --- --- * STATICS --- * UNITS --- * GROUPS --- * CLIENTS --- * AIRBASES --- * PLAYERSJOINED --- * PLAYERS --- * CARGOS --- --- On top, for internal MOOSE administration purposes, the DATBASE administers the Unit and Group TEMPLATES as defined within the Mission Editor. --- --- Moose will automatically create one instance of the DATABASE class into the **global** object _DATABASE. --- Moose refers to _DATABASE within the framework extensively, but you can also refer to the _DATABASE object within your missions if required. --- --- 1.1) DATABASE iterators --- ----------------------- --- You can iterate the database with the available iterator methods. --- The iterator methods will walk the DATABASE set, and call for each element within the set a function that you provide. --- The following iterator methods are currently available within the DATABASE: --- --- * @{#DATABASE.ForEachUnit}: Calls a function for each @{UNIT} it finds within the DATABASE. --- * @{#DATABASE.ForEachGroup}: Calls a function for each @{GROUP} it finds within the DATABASE. --- * @{#DATABASE.ForEachPlayer}: Calls a function for each alive player it finds within the DATABASE. --- * @{#DATABASE.ForEachPlayerJoined}: Calls a function for each joined player it finds within the DATABASE. --- * @{#DATABASE.ForEachClient}: Calls a function for each @{CLIENT} it finds within the DATABASE. --- * @{#DATABASE.ForEachClientAlive}: Calls a function for each alive @{CLIENT} it finds within the DATABASE. --- --- === --- --- --- ### Author: **Sven Van de Velde (FlightControl)** --- ### Contributions: --- --- ==== --- @module Database - - ---- DATABASE class --- @type DATABASE --- @extends Core.Base#BASE -DATABASE = { - ClassName = "DATABASE", - Templates = { - Units = {}, - Groups = {}, - Statics = {}, - ClientsByName = {}, - ClientsByID = {}, - }, - UNITS = {}, - UNITS_Index = {}, - STATICS = {}, - GROUPS = {}, - PLAYERS = {}, - PLAYERSJOINED = {}, - PLAYERUNITS = {}, - CLIENTS = {}, - CARGOS = {}, - AIRBASES = {}, - COUNTRY_ID = {}, - COUNTRY_NAME = {}, - NavPoints = {}, - PLAYERSETTINGS = {}, - ZONENAMES = {}, -} - -local _DATABASECoalition = - { - [1] = "Red", - [2] = "Blue", - } - -local _DATABASECategory = - { - ["plane"] = Unit.Category.AIRPLANE, - ["helicopter"] = Unit.Category.HELICOPTER, - ["vehicle"] = Unit.Category.GROUND_UNIT, - ["ship"] = Unit.Category.SHIP, - ["static"] = Unit.Category.STRUCTURE, - } - - ---- Creates a new DATABASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #DATABASE self --- @return #DATABASE --- @usage --- -- Define a new DATABASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. --- DBObject = DATABASE:New() -function DATABASE:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) -- #DATABASE - - self:SetEventPriority( 1 ) - - self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) - self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.NewCargo ) - self:HandleEvent( EVENTS.DeleteCargo ) - - -- Follow alive players and clients - self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) - self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit ) - - self:_RegisterTemplates() - self:_RegisterGroupsAndUnits() - self:_RegisterClients() - self:_RegisterStatics() - --self:_RegisterPlayers() - self:_RegisterAirbases() - - self.UNITS_Position = 0 - - --- @param #DATABASE self - local function CheckPlayers( self ) - - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - --self:E( { "CoalitionData:", CoalitionData } ) - for UnitId, UnitData in pairs( CoalitionData ) do - if UnitData and UnitData:isExist() then - - local UnitName = UnitData:getName() - local PlayerName = UnitData:getPlayerName() - local PlayerUnit = UNIT:Find( UnitData ) - --self:T( { "UnitData:", UnitData, UnitName, PlayerName, PlayerUnit } ) - - if PlayerName and PlayerName ~= "" then - if self.PLAYERS[PlayerName] == nil or self.PLAYERS[PlayerName] ~= UnitName then - --self:E( { "Add player for unit:", UnitName, PlayerName } ) - self:AddPlayer( UnitName, PlayerName ) - --_EVENTDISPATCHER:CreateEventPlayerEnterUnit( PlayerUnit ) - local Settings = SETTINGS:Set( PlayerName ) - Settings:SetPlayerMenu( PlayerUnit ) - end - end - end - end - end - end - - self:E( "Scheduling" ) - PlayerCheckSchedule = SCHEDULER:New( nil, CheckPlayers, { self }, 1, 1 ) - - return self -end - ---- Finds a Unit based on the Unit Name. --- @param #DATABASE self --- @param #string UnitName --- @return Wrapper.Unit#UNIT The found Unit. -function DATABASE:FindUnit( UnitName ) - - local UnitFound = self.UNITS[UnitName] - return UnitFound -end - - ---- Adds a Unit based on the Unit Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddUnit( DCSUnitName ) - - if not self.UNITS[DCSUnitName] then - local UnitRegister = UNIT:Register( DCSUnitName ) - self.UNITS[DCSUnitName] = UNIT:Register( DCSUnitName ) - - table.insert( self.UNITS_Index, DCSUnitName ) - end - - return self.UNITS[DCSUnitName] -end - - ---- Deletes a Unit from the DATABASE based on the Unit Name. --- @param #DATABASE self -function DATABASE:DeleteUnit( DCSUnitName ) - - self.UNITS[DCSUnitName] = nil -end - ---- Adds a Static based on the Static Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddStatic( DCSStaticName ) - - if not self.STATICS[DCSStaticName] then - self.STATICS[DCSStaticName] = STATIC:Register( DCSStaticName ) - end -end - - ---- Deletes a Static from the DATABASE based on the Static Name. --- @param #DATABASE self -function DATABASE:DeleteStatic( DCSStaticName ) - - --self.STATICS[DCSStaticName] = nil -end - ---- Finds a STATIC based on the StaticName. --- @param #DATABASE self --- @param #string StaticName --- @return Wrapper.Static#STATIC The found STATIC. -function DATABASE:FindStatic( StaticName ) - - local StaticFound = self.STATICS[StaticName] - return StaticFound -end - ---- Finds a AIRBASE based on the AirbaseName. --- @param #DATABASE self --- @param #string AirbaseName --- @return Wrapper.Airbase#AIRBASE The found AIRBASE. -function DATABASE:FindAirbase( AirbaseName ) - - local AirbaseFound = self.AIRBASES[AirbaseName] - return AirbaseFound -end - ---- Adds a Airbase based on the Airbase Name in the DATABASE. --- @param #DATABASE self --- @param #string AirbaseName The name of the airbase -function DATABASE:AddAirbase( AirbaseName ) - - if not self.AIRBASES[AirbaseName] then - self.AIRBASES[AirbaseName] = AIRBASE:Register( AirbaseName ) - end -end - - ---- Deletes a Airbase from the DATABASE based on the Airbase Name. --- @param #DATABASE self --- @param #string AirbaseName The name of the airbase -function DATABASE:DeleteAirbase( AirbaseName ) - - self.AIRBASES[AirbaseName] = nil -end - ---- Finds an AIRBASE based on the AirbaseName. --- @param #DATABASE self --- @param #string AirbaseName --- @return Wrapper.Airbase#AIRBASE The found AIRBASE. -function DATABASE:FindAirbase( AirbaseName ) - - local AirbaseFound = self.AIRBASES[AirbaseName] - return AirbaseFound -end - ---- Adds a Cargo based on the Cargo Name in the DATABASE. --- @param #DATABASE self --- @param #string CargoName The name of the airbase -function DATABASE:AddCargo( Cargo ) - - if not self.CARGOS[Cargo.Name] then - self.CARGOS[Cargo.Name] = Cargo - end -end - - ---- Deletes a Cargo from the DATABASE based on the Cargo Name. --- @param #DATABASE self --- @param #string CargoName The name of the airbase -function DATABASE:DeleteCargo( CargoName ) - - self.CARGOS[CargoName] = nil -end - ---- Finds an CARGO based on the CargoName. --- @param #DATABASE self --- @param #string CargoName --- @return Wrapper.Cargo#CARGO The found CARGO. -function DATABASE:FindCargo( CargoName ) - - local CargoFound = self.CARGOS[CargoName] - return CargoFound -end - - ---- Finds a CLIENT based on the ClientName. --- @param #DATABASE self --- @param #string ClientName --- @return Wrapper.Client#CLIENT The found CLIENT. -function DATABASE:FindClient( ClientName ) - - local ClientFound = self.CLIENTS[ClientName] - return ClientFound -end - - ---- Adds a CLIENT based on the ClientName in the DATABASE. --- @param #DATABASE self -function DATABASE:AddClient( ClientName ) - - if not self.CLIENTS[ClientName] then - self.CLIENTS[ClientName] = CLIENT:Register( ClientName ) - end - - return self.CLIENTS[ClientName] -end - - ---- Finds a GROUP based on the GroupName. --- @param #DATABASE self --- @param #string GroupName --- @return Wrapper.Group#GROUP The found GROUP. -function DATABASE:FindGroup( GroupName ) - - local GroupFound = self.GROUPS[GroupName] - return GroupFound -end - - ---- Adds a GROUP based on the GroupName in the DATABASE. --- @param #DATABASE self -function DATABASE:AddGroup( GroupName ) - - if not self.GROUPS[GroupName] then - self:E( { "Add GROUP:", GroupName } ) - self.GROUPS[GroupName] = GROUP:Register( GroupName ) - end - - return self.GROUPS[GroupName] -end - ---- Adds a player based on the Player Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddPlayer( UnitName, PlayerName ) - - if PlayerName then - self:E( { "Add player for unit:", UnitName, PlayerName } ) - self.PLAYERS[PlayerName] = UnitName - self.PLAYERUNITS[UnitName] = PlayerName - self.PLAYERSJOINED[PlayerName] = PlayerName - end -end - ---- Deletes a player from the DATABASE based on the Player Name. --- @param #DATABASE self -function DATABASE:DeletePlayer( UnitName, PlayerName ) - - if PlayerName then - self:E( { "Clean player:", PlayerName } ) - self.PLAYERS[PlayerName] = nil - self.PLAYERUNITS[UnitName] = PlayerName - end -end - - ---- Instantiate new Groups within the DCSRTE. --- This method expects EXACTLY the same structure as a structure within the ME, and needs 2 additional fields defined: --- SpawnCountryID, SpawnCategoryID --- This method is used by the SPAWN class. --- @param #DATABASE self --- @param #table SpawnTemplate --- @return #DATABASE self -function DATABASE:Spawn( SpawnTemplate ) - self:F( SpawnTemplate.name ) - - self:T( { SpawnTemplate.SpawnCountryID, SpawnTemplate.SpawnCategoryID } ) - - -- Copy the spawn variables of the template in temporary storage, nullify, and restore the spawn variables. - local SpawnCoalitionID = SpawnTemplate.CoalitionID - local SpawnCountryID = SpawnTemplate.CountryID - local SpawnCategoryID = SpawnTemplate.CategoryID - - -- Nullify - SpawnTemplate.CoalitionID = nil - SpawnTemplate.CountryID = nil - SpawnTemplate.CategoryID = nil - - self:_RegisterGroupTemplate( SpawnTemplate, SpawnCoalitionID, SpawnCategoryID, SpawnCountryID ) - - self:T3( SpawnTemplate ) - coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) - - -- Restore - SpawnTemplate.CoalitionID = SpawnCoalitionID - SpawnTemplate.CountryID = SpawnCountryID - SpawnTemplate.CategoryID = SpawnCategoryID - - -- Ensure that for the spawned group and its units, there are GROUP and UNIT objects created in the DATABASE. - local SpawnGroup = self:AddGroup( SpawnTemplate.name ) - for UnitID, UnitData in pairs( SpawnTemplate.units ) do - self:AddUnit( UnitData.name ) - end - - return SpawnGroup -end - ---- Set a status to a Group within the Database, this to check crossing events for example. -function DATABASE:SetStatusGroup( GroupName, Status ) - self:F2( Status ) - - self.Templates.Groups[GroupName].Status = Status -end - ---- Get a status to a Group within the Database, this to check crossing events for example. -function DATABASE:GetStatusGroup( GroupName ) - self:F2( Status ) - - if self.Templates.Groups[GroupName] then - return self.Templates.Groups[GroupName].Status - else - return "" - end -end - ---- Private method that registers new Group Templates within the DATABASE Object. --- @param #DATABASE self --- @param #table GroupTemplate --- @return #DATABASE self -function DATABASE:_RegisterGroupTemplate( GroupTemplate, CoalitionID, CategoryID, CountryID ) - - local GroupTemplateName = env.getValueDictByKey(GroupTemplate.name) - - local TraceTable = {} - - if not self.Templates.Groups[GroupTemplateName] then - self.Templates.Groups[GroupTemplateName] = {} - self.Templates.Groups[GroupTemplateName].Status = nil - end - - -- Delete the spans from the route, it is not needed and takes memory. - if GroupTemplate.route and GroupTemplate.route.spans then - GroupTemplate.route.spans = nil - end - - GroupTemplate.CategoryID = CategoryID - GroupTemplate.CoalitionID = CoalitionID - GroupTemplate.CountryID = CountryID - - self.Templates.Groups[GroupTemplateName].GroupName = GroupTemplateName - self.Templates.Groups[GroupTemplateName].Template = GroupTemplate - self.Templates.Groups[GroupTemplateName].groupId = GroupTemplate.groupId - self.Templates.Groups[GroupTemplateName].UnitCount = #GroupTemplate.units - self.Templates.Groups[GroupTemplateName].Units = GroupTemplate.units - self.Templates.Groups[GroupTemplateName].CategoryID = CategoryID - self.Templates.Groups[GroupTemplateName].CoalitionID = CoalitionID - self.Templates.Groups[GroupTemplateName].CountryID = CountryID - - - TraceTable[#TraceTable+1] = "Group" - TraceTable[#TraceTable+1] = self.Templates.Groups[GroupTemplateName].GroupName - - TraceTable[#TraceTable+1] = "Coalition" - TraceTable[#TraceTable+1] = self.Templates.Groups[GroupTemplateName].CoalitionID - TraceTable[#TraceTable+1] = "Category" - TraceTable[#TraceTable+1] = self.Templates.Groups[GroupTemplateName].CategoryID - TraceTable[#TraceTable+1] = "Country" - TraceTable[#TraceTable+1] = self.Templates.Groups[GroupTemplateName].CountryID - - TraceTable[#TraceTable+1] = "Units" - - for unit_num, UnitTemplate in pairs( GroupTemplate.units ) do - - UnitTemplate.name = env.getValueDictByKey(UnitTemplate.name) - - self.Templates.Units[UnitTemplate.name] = {} - self.Templates.Units[UnitTemplate.name].UnitName = UnitTemplate.name - self.Templates.Units[UnitTemplate.name].Template = UnitTemplate - self.Templates.Units[UnitTemplate.name].GroupName = GroupTemplateName - self.Templates.Units[UnitTemplate.name].GroupTemplate = GroupTemplate - self.Templates.Units[UnitTemplate.name].GroupId = GroupTemplate.groupId - self.Templates.Units[UnitTemplate.name].CategoryID = CategoryID - self.Templates.Units[UnitTemplate.name].CoalitionID = CoalitionID - self.Templates.Units[UnitTemplate.name].CountryID = CountryID - - if UnitTemplate.skill and (UnitTemplate.skill == "Client" or UnitTemplate.skill == "Player") then - self.Templates.ClientsByName[UnitTemplate.name] = UnitTemplate - self.Templates.ClientsByName[UnitTemplate.name].CategoryID = CategoryID - self.Templates.ClientsByName[UnitTemplate.name].CoalitionID = CoalitionID - self.Templates.ClientsByName[UnitTemplate.name].CountryID = CountryID - self.Templates.ClientsByID[UnitTemplate.unitId] = UnitTemplate - end - - TraceTable[#TraceTable+1] = self.Templates.Units[UnitTemplate.name].UnitName - end - - self:E( TraceTable ) -end - -function DATABASE:GetGroupTemplate( GroupName ) - local GroupTemplate = self.Templates.Groups[GroupName].Template - GroupTemplate.SpawnCoalitionID = self.Templates.Groups[GroupName].CoalitionID - GroupTemplate.SpawnCategoryID = self.Templates.Groups[GroupName].CategoryID - GroupTemplate.SpawnCountryID = self.Templates.Groups[GroupName].CountryID - return GroupTemplate -end - ---- Private method that registers new Static Templates within the DATABASE Object. --- @param #DATABASE self --- @param #table GroupTemplate --- @return #DATABASE self -function DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, CategoryID, CountryID ) - - local TraceTable = {} - - local StaticTemplateName = env.getValueDictByKey(StaticTemplate.name) - - self.Templates.Statics[StaticTemplateName] = self.Templates.Statics[StaticTemplateName] or {} - - StaticTemplate.CategoryID = CategoryID - StaticTemplate.CoalitionID = CoalitionID - StaticTemplate.CountryID = CountryID - - self.Templates.Statics[StaticTemplateName].StaticName = StaticTemplateName - self.Templates.Statics[StaticTemplateName].GroupTemplate = StaticTemplate - self.Templates.Statics[StaticTemplateName].UnitTemplate = StaticTemplate.units[1] - self.Templates.Statics[StaticTemplateName].CategoryID = CategoryID - self.Templates.Statics[StaticTemplateName].CoalitionID = CoalitionID - self.Templates.Statics[StaticTemplateName].CountryID = CountryID - - - TraceTable[#TraceTable+1] = "Static" - TraceTable[#TraceTable+1] = self.Templates.Statics[StaticTemplateName].GroupName - - TraceTable[#TraceTable+1] = "Coalition" - TraceTable[#TraceTable+1] = self.Templates.Statics[StaticTemplateName].CoalitionID - TraceTable[#TraceTable+1] = "Category" - TraceTable[#TraceTable+1] = self.Templates.Statics[StaticTemplateName].CategoryID - TraceTable[#TraceTable+1] = "Country" - TraceTable[#TraceTable+1] = self.Templates.Statics[StaticTemplateName].CountryID - - self:E( TraceTable ) -end - - ---- @param #DATABASE self -function DATABASE:GetStaticUnitTemplate( StaticName ) - local StaticTemplate = self.Templates.Statics[StaticName].UnitTemplate - StaticTemplate.SpawnCoalitionID = self.Templates.Statics[StaticName].CoalitionID - StaticTemplate.SpawnCategoryID = self.Templates.Statics[StaticName].CategoryID - StaticTemplate.SpawnCountryID = self.Templates.Statics[StaticName].CountryID - return StaticTemplate -end - - -function DATABASE:GetGroupNameFromUnitName( UnitName ) - return self.Templates.Units[UnitName].GroupName -end - -function DATABASE:GetGroupTemplateFromUnitName( UnitName ) - return self.Templates.Units[UnitName].GroupTemplate -end - -function DATABASE:GetCoalitionFromClientTemplate( ClientName ) - return self.Templates.ClientsByName[ClientName].CoalitionID -end - -function DATABASE:GetCategoryFromClientTemplate( ClientName ) - return self.Templates.ClientsByName[ClientName].CategoryID -end - -function DATABASE:GetCountryFromClientTemplate( ClientName ) - return self.Templates.ClientsByName[ClientName].CountryID -end - ---- Airbase - -function DATABASE:GetCoalitionFromAirbase( AirbaseName ) - return self.AIRBASES[AirbaseName]:GetCoalition() -end - -function DATABASE:GetCategoryFromAirbase( AirbaseName ) - return self.AIRBASES[AirbaseName]:GetCategory() -end - - - ---- Private method that registers all alive players in the mission. --- @param #DATABASE self --- @return #DATABASE self -function DATABASE:_RegisterPlayers() - - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - for UnitId, UnitData in pairs( CoalitionData ) do - self:T3( { "UnitData:", UnitData } ) - if UnitData and UnitData:isExist() then - local UnitName = UnitData:getName() - local PlayerName = UnitData:getPlayerName() - if not self.PLAYERS[PlayerName] then - self:E( { "Add player for unit:", UnitName, PlayerName } ) - self:AddPlayer( UnitName, PlayerName ) - end - end - end - end - - return self -end - - ---- Private method that registers all Groups and Units within in the mission. --- @param #DATABASE self --- @return #DATABASE self -function DATABASE:_RegisterGroupsAndUnits() - - local CoalitionsData = { GroupsRed = coalition.getGroups( coalition.side.RED ), GroupsBlue = coalition.getGroups( coalition.side.BLUE ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - for DCSGroupId, DCSGroup in pairs( CoalitionData ) do - - if DCSGroup:isExist() then - local DCSGroupName = DCSGroup:getName() - - self:E( { "Register Group:", DCSGroupName } ) - self:AddGroup( DCSGroupName ) - - for DCSUnitId, DCSUnit in pairs( DCSGroup:getUnits() ) do - - local DCSUnitName = DCSUnit:getName() - self:E( { "Register Unit:", DCSUnitName } ) - self:AddUnit( DCSUnitName ) - end - else - self:E( { "Group does not exist: ", DCSGroup } ) - end - - end - end - - return self -end - ---- Private method that registers all Units of skill Client or Player within in the mission. --- @param #DATABASE self --- @return #DATABASE self -function DATABASE:_RegisterClients() - - for ClientName, ClientTemplate in pairs( self.Templates.ClientsByName ) do - self:E( { "Register Client:", ClientName } ) - self:AddClient( ClientName ) - end - - return self -end - ---- @param #DATABASE self -function DATABASE:_RegisterStatics() - - local CoalitionsData = { GroupsRed = coalition.getStaticObjects( coalition.side.RED ), GroupsBlue = coalition.getStaticObjects( coalition.side.BLUE ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - for DCSStaticId, DCSStatic in pairs( CoalitionData ) do - - if DCSStatic:isExist() then - local DCSStaticName = DCSStatic:getName() - - self:E( { "Register Static:", DCSStaticName } ) - self:AddStatic( DCSStaticName ) - else - self:E( { "Static does not exist: ", DCSStatic } ) - end - end - end - - return self -end - ---- @param #DATABASE self -function DATABASE:_RegisterAirbases() - - local CoalitionsData = { AirbasesRed = coalition.getAirbases( coalition.side.RED ), AirbasesBlue = coalition.getAirbases( coalition.side.BLUE ), AirbasesNeutral = coalition.getAirbases( coalition.side.NEUTRAL ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - for DCSAirbaseId, DCSAirbase in pairs( CoalitionData ) do - - local DCSAirbaseName = DCSAirbase:getName() - - self:E( { "Register Airbase:", DCSAirbaseName } ) - self:AddAirbase( DCSAirbaseName ) - end - end - - return self -end - - ---- Events - ---- Handles the OnBirth event for the alive units set. --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnBirth( Event ) - self:F2( { Event } ) - - if Event.IniDCSUnit then - if Event.IniObjectCategory == 3 then - self:AddStatic( Event.IniDCSUnitName ) - else - if Event.IniObjectCategory == 1 then - self:AddUnit( Event.IniDCSUnitName ) - self:AddGroup( Event.IniDCSGroupName ) - end - end - --self:_EventOnPlayerEnterUnit( Event ) - end -end - - ---- Handles the OnDead or OnCrash event for alive units set. --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnDeadOrCrash( Event ) - self:F2( { Event } ) - - if Event.IniDCSUnit then - if Event.IniObjectCategory == 3 then - if self.STATICS[Event.IniDCSUnitName] then - self:DeleteStatic( Event.IniDCSUnitName ) - end - else - if Event.IniObjectCategory == 1 then - if self.UNITS[Event.IniDCSUnitName] then - self:DeleteUnit( Event.IniDCSUnitName ) - end - end - end - end -end - - ---- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnPlayerEnterUnit( Event ) - self:F2( { Event } ) - - if Event.IniUnit then - if Event.IniObjectCategory == 1 then - self:AddUnit( Event.IniDCSUnitName ) - self:AddGroup( Event.IniDCSGroupName ) - local PlayerName = Event.IniUnit:GetPlayerName() - if not self.PLAYERS[PlayerName] then - self:AddPlayer( Event.IniUnitName, PlayerName ) - end - local Settings = SETTINGS:Set( PlayerName ) - Settings:SetPlayerMenu( Event.IniUnit ) - end - end -end - - ---- Handles the OnPlayerLeaveUnit event to clean the active players table. --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnPlayerLeaveUnit( Event ) - self:F2( { Event } ) - - if Event.IniUnit then - if Event.IniObjectCategory == 1 then - local PlayerName = Event.IniUnit:GetPlayerName() - if self.PLAYERS[PlayerName] then - local Settings = SETTINGS:Set( PlayerName ) - Settings:RemovePlayerMenu( Event.IniUnit ) - self:DeletePlayer( Event.IniUnit, PlayerName ) - end - end - end -end - ---- Iterators - ---- Iterate the DATABASE and call an iterator function for the given set, providing the Object for each element within the set and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive player in the database. --- @return #DATABASE self -function DATABASE:ForEach( IteratorFunction, FinalizeFunction, arg, Set ) - self:F2( arg ) - - local function CoRoutine() - local Count = 0 - for ObjectID, Object in pairs( Set ) do - self:T2( Object ) - IteratorFunction( Object, unpack( arg ) ) - Count = Count + 1 --- if Count % 100 == 0 then --- coroutine.yield( false ) --- end - end - return true - end - --- local co = coroutine.create( CoRoutine ) - local co = CoRoutine - - local function Schedule() - --- local status, res = coroutine.resume( co ) - local status, res = co() - self:T3( { status, res } ) - - if status == false then - error( res ) - end - if res == false then - return true -- resume next time the loop - end - if FinalizeFunction then - FinalizeFunction( unpack( arg ) ) - end - return false - end - - local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each **alive** STATIC, providing the STATIC and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a STATIC parameter. --- @return #DATABASE self -function DATABASE:ForEachStatic( IteratorFunction, FinalizeFunction, ... ) --R2.1 - self:F2( arg ) - - self:ForEach( IteratorFunction, FinalizeFunction, arg, self.STATICS ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each **alive** UNIT, providing the UNIT and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a UNIT parameter. --- @return #DATABASE self -function DATABASE:ForEachUnit( IteratorFunction, FinalizeFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, FinalizeFunction, arg, self.UNITS ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each **alive** GROUP, providing the GROUP and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a GROUP parameter. --- @return #DATABASE self -function DATABASE:ForEachGroup( IteratorFunction, FinalizeFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, FinalizeFunction, arg, self.GROUPS ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each **ALIVE** player, providing the player name and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept the player name. --- @return #DATABASE self -function DATABASE:ForEachPlayer( IteratorFunction, FinalizeFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, FinalizeFunction, arg, self.PLAYERS ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each player who has joined the mission, providing the Unit of the player and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a UNIT parameter. --- @return #DATABASE self -function DATABASE:ForEachPlayerJoined( IteratorFunction, FinalizeFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, FinalizeFunction, arg, self.PLAYERSJOINED ) - - return self -end - ---- Iterate the DATABASE and call an iterator function for each **ALIVE** player UNIT, providing the player UNIT and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept the player name. --- @return #DATABASE self -function DATABASE:ForEachPlayerUnit( IteratorFunction, FinalizeFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, FinalizeFunction, arg, self.PLAYERUNITS ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each CLIENT, providing the CLIENT to the function and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called object in the database. The function needs to accept a CLIENT parameter. --- @return #DATABASE self -function DATABASE:ForEachClient( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.CLIENTS ) - - return self -end - ---- Iterate the DATABASE and call an iterator function for each CARGO, providing the CARGO object to the function and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a CLIENT parameter. --- @return #DATABASE self -function DATABASE:ForEachCargo( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.CARGOS ) - - return self -end - - ---- Handles the OnEventNewCargo event. --- @param #DATABASE self --- @param Core.Event#EVENTDATA EventData -function DATABASE:OnEventNewCargo( EventData ) - self:F2( { EventData } ) - - if EventData.Cargo then - self:AddCargo( EventData.Cargo ) - end -end - - ---- Handles the OnEventDeleteCargo. --- @param #DATABASE self --- @param Core.Event#EVENTDATA EventData -function DATABASE:OnEventDeleteCargo( EventData ) - self:F2( { EventData } ) - - if EventData.Cargo then - self:DeleteCargo( EventData.Cargo.Name ) - end -end - - ---- Gets the player settings --- @param #DATABASE self --- @param #string PlayerName --- @return Core.Settings#SETTINGS -function DATABASE:GetPlayerSettings( PlayerName ) - self:F2( { PlayerName } ) - return self.PLAYERSETTINGS[PlayerName] -end - - ---- Sets the player settings --- @param #DATABASE self --- @param #string PlayerName --- @param Core.Settings#SETTINGS Settings --- @return Core.Settings#SETTINGS -function DATABASE:SetPlayerSettings( PlayerName, Settings ) - self:F2( { PlayerName, Settings } ) - self.PLAYERSETTINGS[PlayerName] = Settings -end - - - - ---- @param #DATABASE self -function DATABASE:_RegisterTemplates() - self:F2() - - self.Navpoints = {} - self.UNITS = {} - --Build routines.db.units and self.Navpoints - for CoalitionName, coa_data in pairs(env.mission.coalition) do - - if (CoalitionName == 'red' or CoalitionName == 'blue') and type(coa_data) == 'table' then - --self.Units[coa_name] = {} - - local CoalitionSide = coalition.side[string.upper(CoalitionName)] - - ---------------------------------------------- - -- build nav points DB - self.Navpoints[CoalitionName] = {} - if coa_data.nav_points then --navpoints - for nav_ind, nav_data in pairs(coa_data.nav_points) do - - if type(nav_data) == 'table' then - self.Navpoints[CoalitionName][nav_ind] = routines.utils.deepCopy(nav_data) - - self.Navpoints[CoalitionName][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. - self.Navpoints[CoalitionName][nav_ind]['point'] = {} -- point is used by SSE, support it. - self.Navpoints[CoalitionName][nav_ind]['point']['x'] = nav_data.x - self.Navpoints[CoalitionName][nav_ind]['point']['y'] = 0 - self.Navpoints[CoalitionName][nav_ind]['point']['z'] = nav_data.y - end - end - end - ------------------------------------------------- - if coa_data.country then --there is a country table - for cntry_id, cntry_data in pairs(coa_data.country) do - - local CountryName = string.upper(cntry_data.name) - local CountryID = cntry_data.id - - self.COUNTRY_ID[CountryName] = CountryID - self.COUNTRY_NAME[CountryID] = CountryName - - --self.Units[coa_name][countryName] = {} - --self.Units[coa_name][countryName]["countryId"] = cntry_data.id - - if type(cntry_data) == 'table' then --just making sure - - 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" or obj_type_name == "static" then --should be an unncessary check - - local CategoryName = obj_type_name - - 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! - - --self.Units[coa_name][countryName][category] = {} - - for group_num, Template in pairs(obj_type_data.group) do - - if obj_type_name ~= "static" and Template and Template.units and type(Template.units) == 'table' then --making sure again- this is a valid group - self:_RegisterGroupTemplate( - Template, - CoalitionSide, - _DATABASECategory[string.lower(CategoryName)], - CountryID - ) - else - self:_RegisterStaticTemplate( - Template, - CoalitionSide, - _DATABASECategory[string.lower(CategoryName)], - CountryID - ) - end --if GroupTemplate and GroupTemplate.units then - end --for group_num, GroupTemplate in pairs(obj_type_data.group) do - end --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 - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --if type(cntry_data) == 'table' then - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do - - for ZoneID, ZoneData in pairs( env.mission.triggers.zones ) do - local ZoneName = ZoneData.name - self.ZONENAMES[ZoneName] = ZoneName - end - - return self -end - - - - ---- **Core** -- SET_ classes define **collections** of objects to perform **bulk actions** and logically **group** objects. --- --- ![Banner Image](..\Presentations\SET\Dia1.JPG) --- --- === --- --- SET_ classes group objects of the same type into a collection, which is either: --- --- * Manually managed using the **:Add...()** or **:Remove...()** methods. The initial SET can be filtered with the **@{#SET_BASE.FilterOnce}()** method --- * Dynamically updated when new objects are created or objects are destroyed using the **@{#SET_BASE.FilterStart}()** method. --- --- Various types of SET_ classes are available: --- --- * @{#SET_UNIT}: Defines a colleciton of @{Unit}s filtered by filter criteria. --- * @{#SET_GROUP}: Defines a collection of @{Group}s filtered by filter criteria. --- * @{#SET_CLIENT}: Defines a collection of @{Client}s filterd by filter criteria. --- * @{#SET_AIRBASE}: Defines a collection of @{Airbase}s filtered by filter criteria. --- --- These classes are derived from @{#SET_BASE}, which contains the main methods to manage SETs. --- --- A multitude of other methods are available in SET_ classes that allow to: --- --- * Validate the presence of objects in the SET. --- * Trigger events when objects in the SET change a zone presence. --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- ### Contributions: --- --- ==== --- --- @module Set - - ---- @type SET_BASE --- @field #table Filter --- @field #table Set --- @field #table List --- @field Core.Scheduler#SCHEDULER CallScheduler --- @extends Core.Base#BASE - - ---- # 1) SET_BASE class, extends @{Base#BASE} --- The @{Set#SET_BASE} class defines the core functions that define a collection of objects. --- A SET provides iterators to iterate the SET, but will **temporarily** yield the ForEach interator loop at defined **"intervals"** to the mail simulator loop. --- In this way, large loops can be done while not blocking the simulator main processing loop. --- The default **"yield interval"** is after 10 objects processed. --- The default **"time interval"** is after 0.001 seconds. --- --- ## 1.1) Add or remove objects from the SET --- --- Some key core functions are @{Set#SET_BASE.Add} and @{Set#SET_BASE.Remove} to add or remove objects from the SET in your logic. --- --- ## 1.2) Define the SET iterator **"yield interval"** and the **"time interval"** --- --- Modify the iterator intervals with the @{Set#SET_BASE.SetInteratorIntervals} method. --- You can set the **"yield interval"**, and the **"time interval"**. (See above). --- --- @field #SET_BASE SET_BASE -SET_BASE = { - ClassName = "SET_BASE", - Filter = {}, - Set = {}, - List = {}, - Index = {}, -} - - ---- Creates a new SET_BASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #SET_BASE self --- @return #SET_BASE --- @usage --- -- Define a new SET_BASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. --- DBObject = SET_BASE:New() -function SET_BASE:New( Database ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) -- Core.Set#SET_BASE - - self.Database = Database - - self.YieldInterval = 10 - self.TimeInterval = 0.001 - - self.Set = {} - self.Index = {} - - self.CallScheduler = SCHEDULER:New( self ) - - self:SetEventPriority( 2 ) - - return self -end - ---- Finds an @{Base#BASE} object based on the object Name. --- @param #SET_BASE self --- @param #string ObjectName --- @return Core.Base#BASE The Object found. -function SET_BASE:_Find( ObjectName ) - - local ObjectFound = self.Set[ObjectName] - return ObjectFound -end - - ---- Gets the Set. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:GetSet() - self:F2() - - return self.Set -end - ---- Adds a @{Base#BASE} object in the @{Set#SET_BASE}, using a given ObjectName as the index. --- @param #SET_BASE self --- @param #string ObjectName --- @param Core.Base#BASE Object --- @return Core.Base#BASE The added BASE Object. -function SET_BASE:Add( ObjectName, Object ) - self:F( ObjectName ) - - self.Set[ObjectName] = Object - table.insert( self.Index, ObjectName ) -end - ---- Adds a @{Base#BASE} object in the @{Set#SET_BASE}, using the Object Name as the index. --- @param #SET_BASE self --- @param Wrapper.Object#OBJECT Object --- @return Core.Base#BASE The added BASE Object. -function SET_BASE:AddObject( Object ) - self:F2( Object.ObjectName ) - - self:T( Object.UnitName ) - self:T( Object.ObjectName ) - self:Add( Object.ObjectName, Object ) - -end - - - ---- Removes a @{Base#BASE} object from the @{Set#SET_BASE} and derived classes, based on the Object Name. --- @param #SET_BASE self --- @param #string ObjectName -function SET_BASE:Remove( ObjectName ) - - local Object = self.Set[ObjectName] - - self:F3( { ObjectName, Object } ) - - if Object then - for Index, Key in ipairs( self.Index ) do - if Key == ObjectName then - table.remove( self.Index, Index ) - self.Set[ObjectName] = nil - break - end - end - - end - -end - ---- Gets a @{Base#BASE} object from the @{Set#SET_BASE} and derived classes, based on the Object Name. --- @param #SET_BASE self --- @param #string ObjectName --- @return Core.Base#BASE -function SET_BASE:Get( ObjectName ) - self:F( ObjectName ) - - local Object = self.Set[ObjectName] - - self:T3( { ObjectName, Object } ) - return Object -end - ---- Gets the first object from the @{Set#SET_BASE} and derived classes. --- @param #SET_BASE self --- @return Core.Base#BASE -function SET_BASE:GetFirst() - - local ObjectName = self.Index[1] - local FirstObject = self.Set[ObjectName] - self:T3( { FirstObject } ) - return FirstObject -end - ---- Gets the last object from the @{Set#SET_BASE} and derived classes. --- @param #SET_BASE self --- @return Core.Base#BASE -function SET_BASE:GetLast() - - local ObjectName = self.Index[#self.Index] - local LastObject = self.Set[ObjectName] - self:T3( { LastObject } ) - return LastObject -end - ---- Gets a random object from the @{Set#SET_BASE} and derived classes. --- @param #SET_BASE self --- @return Core.Base#BASE -function SET_BASE:GetRandom() - - local RandomItem = self.Set[self.Index[math.random(#self.Index)]] - self:T3( { RandomItem } ) - return RandomItem -end - - ---- Retrieves the amount of objects in the @{Set#SET_BASE} and derived classes. --- @param #SET_BASE self --- @return #number Count -function SET_BASE:Count() - - return self.Index and #self.Index or 0 -end - - ---- Copies the Filter criteria from a given Set (for rebuilding a new Set based on an existing Set). --- @param #SET_BASE self --- @param #SET_BASE BaseSet --- @return #SET_BASE -function SET_BASE:SetDatabase( BaseSet ) - - -- Copy the filter criteria of the BaseSet - local OtherFilter = routines.utils.deepCopy( BaseSet.Filter ) - self.Filter = OtherFilter - - -- Now base the new Set on the BaseSet - self.Database = BaseSet:GetSet() - return self -end - - - ---- Define the SET iterator **"yield interval"** and the **"time interval"**. --- @param #SET_BASE self --- @param #number YieldInterval Sets the frequency when the iterator loop will yield after the number of objects processed. The default frequency is 10 objects processed. --- @param #number TimeInterval Sets the time in seconds when the main logic will resume the iterator loop. The default time is 0.001 seconds. --- @return #SET_BASE self -function SET_BASE:SetIteratorIntervals( YieldInterval, TimeInterval ) - - self.YieldInterval = YieldInterval - self.TimeInterval = TimeInterval - - return self -end - - ---- Filters for the defined collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:FilterOnce() - - for ObjectName, Object in pairs( self.Database ) do - - if self:IsIncludeObject( Object ) then - self:Add( ObjectName, Object ) - end - end - - return self -end - ---- Starts the filtering for the defined collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:_FilterStart() - - for ObjectName, Object in pairs( self.Database ) do - - if self:IsIncludeObject( Object ) then - self:E( { "Adding Object:", ObjectName } ) - self:Add( ObjectName, Object ) - end - end - - self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) - self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) - - -- Follow alive players and clients - self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) - self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit ) - - - return self -end - ---- Starts the filtering of the Dead events for the collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:FilterDeads() --R2.1 allow deads to be filtered to automatically handle deads in the collection. - - self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) - - return self -end - ---- Starts the filtering of the Crash events for the collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:FilterCrashes() --R2.1 allow crashes to be filtered to automatically handle crashes in the collection. - - self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) - - return self -end - ---- Stops the filtering for the defined collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:FilterStop() - - self:UnHandleEvent( EVENTS.Birth ) - self:UnHandleEvent( EVENTS.Dead ) - self:UnHandleEvent( EVENTS.Crash ) - - return self -end - ---- Iterate the SET_BASE while identifying the nearest object from a @{Point#POINT_VEC2}. --- @param #SET_BASE self --- @param Core.Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest object in the set. --- @return Core.Base#BASE The closest object. -function SET_BASE:FindNearestObjectFromPointVec2( PointVec2 ) - self:F2( PointVec2 ) - - local NearestObject = nil - local ClosestDistance = nil - - for ObjectID, ObjectData in pairs( self.Set ) do - if NearestObject == nil then - NearestObject = ObjectData - ClosestDistance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) - else - local Distance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) - if Distance < ClosestDistance then - NearestObject = ObjectData - ClosestDistance = Distance - end - end - end - - return NearestObject -end - - - ------ Private method that registers all alive players in the mission. ----- @param #SET_BASE self ----- @return #SET_BASE self ---function SET_BASE:_RegisterPlayers() --- --- local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } --- for CoalitionId, CoalitionData in pairs( CoalitionsData ) do --- for UnitId, UnitData in pairs( CoalitionData ) do --- self:T3( { "UnitData:", UnitData } ) --- if UnitData and UnitData:isExist() then --- local UnitName = UnitData:getName() --- if not self.PlayersAlive[UnitName] then --- self:E( { "Add player for unit:", UnitName, UnitData:getPlayerName() } ) --- self.PlayersAlive[UnitName] = UnitData:getPlayerName() --- end --- end --- end --- end --- --- return self ---end - ---- Events - ---- Handles the OnBirth event for the Set. --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnBirth( Event ) - self:F3( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:AddInDatabase( Event ) - self:T3( ObjectName, Object ) - if Object and self:IsIncludeObject( Object ) then - self:Add( ObjectName, Object ) - --self:_EventOnPlayerEnterUnit( Event ) - end - end -end - ---- Handles the OnDead or OnCrash event for alive units set. --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnDeadOrCrash( Event ) - self:F3( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:FindInDatabase( Event ) - if ObjectName then - self:Remove( ObjectName ) - end - end -end - ---- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnPlayerEnterUnit( Event ) - self:F3( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:AddInDatabase( Event ) - self:T3( ObjectName, Object ) - if self:IsIncludeObject( Object ) then - self:Add( ObjectName, Object ) - --self:_EventOnPlayerEnterUnit( Event ) - end - end -end - ---- Handles the OnPlayerLeaveUnit event to clean the active players table. --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnPlayerLeaveUnit( Event ) - self:F3( { Event } ) - - local ObjectName = Event.IniDCSUnit - if Event.IniDCSUnit then - if Event.IniDCSGroup then - local GroupUnits = Event.IniDCSGroup:getUnits() - local PlayerCount = 0 - for _, DCSUnit in pairs( GroupUnits ) do - if DCSUnit ~= Event.IniDCSUnit then - if DCSUnit:getPlayerName() ~= nil then - PlayerCount = PlayerCount + 1 - end - end - end - self:E(PlayerCount) - if PlayerCount == 0 then - self:Remove( Event.IniDCSGroupName ) - end - end - end -end - --- Iterators - ---- Iterate the SET_BASE and derived classes and call an iterator function for the given SET_BASE, providing the Object for each element within the set and optional parameters. --- @param #SET_BASE self --- @param #function IteratorFunction The function that will be called. --- @return #SET_BASE self -function SET_BASE:ForEach( IteratorFunction, arg, Set, Function, FunctionArguments ) - self:F3( arg ) - - Set = Set or self:GetSet() - arg = arg or {} - - local function CoRoutine() - local Count = 0 - for ObjectID, ObjectData in pairs( Set ) do - local Object = ObjectData - self:T3( Object ) - if Function then - if Function( unpack( FunctionArguments ), Object ) == true then - IteratorFunction( Object, unpack( arg ) ) - end - else - IteratorFunction( Object, unpack( arg ) ) - end - Count = Count + 1 --- if Count % self.YieldInterval == 0 then --- coroutine.yield( false ) --- end - end - return true - end - --- local co = coroutine.create( CoRoutine ) - local co = CoRoutine - - local function Schedule() - --- local status, res = coroutine.resume( co ) - local status, res = co() - self:T3( { status, res } ) - - if status == false then - error( res ) - end - if res == false then - return true -- resume next time the loop - end - - return false - end - - --self.CallScheduler:Schedule( self, Schedule, {}, self.TimeInterval, self.TimeInterval, 0 ) - Schedule() - - return self -end - - ------ Iterate the SET_BASE and call an interator function for each **alive** unit, providing the Unit and optional parameters. ----- @param #SET_BASE self ----- @param #function IteratorFunction The function that will be called when there is an alive unit in the SET_BASE. The function needs to accept a UNIT parameter. ----- @return #SET_BASE self ---function SET_BASE:ForEachDCSUnitAlive( IteratorFunction, ... ) --- self:F3( arg ) --- --- self:ForEach( IteratorFunction, arg, self.DCSUnitsAlive ) --- --- return self ---end --- ------ Iterate the SET_BASE and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. ----- @param #SET_BASE self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_BASE. The function needs to accept a UNIT parameter. ----- @return #SET_BASE self ---function SET_BASE:ForEachPlayer( IteratorFunction, ... ) --- self:F3( arg ) --- --- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) --- --- return self ---end --- --- ------ Iterate the SET_BASE and call an interator function for each client, providing the Client to the function and optional parameters. ----- @param #SET_BASE self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_BASE. The function needs to accept a CLIENT parameter. ----- @return #SET_BASE self ---function SET_BASE:ForEachClient( IteratorFunction, ... ) --- self:F3( arg ) --- --- self:ForEach( IteratorFunction, arg, self.Clients ) --- --- return self ---end - - ---- Decides whether to include the Object --- @param #SET_BASE self --- @param #table Object --- @return #SET_BASE self -function SET_BASE:IsIncludeObject( Object ) - self:F3( Object ) - - return true -end - ---- Gets a string with all the object names. --- @param #SET_BASE self --- @return #string A string with the names of the objects. -function SET_BASE:GetObjectNames() - self:F3() - - local ObjectNames = "" - for ObjectName, Object in pairs( self.Set ) do - ObjectNames = ObjectNames .. ObjectName .. ", " - end - - return ObjectNames -end - ---- Flushes the current SET_BASE contents in the log ... (for debugging reasons). --- @param #SET_BASE self --- @return #string A string with the names of the objects. -function SET_BASE:Flush() - self:F3() - - local ObjectNames = "" - for ObjectName, Object in pairs( self.Set ) do - ObjectNames = ObjectNames .. ObjectName .. ", " - end - self:E( { "Objects in Set:", ObjectNames } ) - - return ObjectNames -end - - ---- @type SET_GROUP --- @extends Core.Set#SET_BASE - ---- # SET_GROUP class, extends @{Set#SET_BASE} --- --- Mission designers can use the @{Set#SET_GROUP} class to build sets of groups belonging to certain: --- --- * Coalitions --- * Categories --- * Countries --- * Starting with certain prefix strings. --- --- ## 1. SET_GROUP constructor --- --- Create a new SET_GROUP object with the @{#SET_GROUP.New} method: --- --- * @{#SET_GROUP.New}: Creates a new SET_GROUP object. --- --- ## 2. Add or Remove GROUP(s) from SET_GROUP --- --- GROUPS can be added and removed using the @{Set#SET_GROUP.AddGroupsByName} and @{Set#SET_GROUP.RemoveGroupsByName} respectively. --- These methods take a single GROUP name or an array of GROUP names to be added or removed from SET_GROUP. --- --- ## 3. SET_GROUP filter criteria --- --- You can set filter criteria to define the set of groups within the SET_GROUP. --- Filter criteria are defined by: --- --- * @{#SET_GROUP.FilterCoalitions}: Builds the SET_GROUP with the groups belonging to the coalition(s). --- * @{#SET_GROUP.FilterCategories}: Builds the SET_GROUP with the groups belonging to the category(ies). --- * @{#SET_GROUP.FilterCountries}: Builds the SET_GROUP with the gruops belonging to the country(ies). --- * @{#SET_GROUP.FilterPrefixes}: Builds the SET_GROUP with the groups starting with the same prefix string(s). --- --- For the Category Filter, extra methods have been added: --- --- * @{#SET_GROUP.FilterCategoryAirplane}: Builds the SET_GROUP from airplanes. --- * @{#SET_GROUP.FilterCategoryHelicopter}: Builds the SET_GROUP from helicopters. --- * @{#SET_GROUP.FilterCategoryGround}: Builds the SET_GROUP from ground vehicles or infantry. --- * @{#SET_GROUP.FilterCategoryShip}: Builds the SET_GROUP from ships. --- * @{#SET_GROUP.FilterCategoryStructure}: Builds the SET_GROUP from structures. --- --- --- Once the filter criteria have been set for the SET_GROUP, you can start filtering using: --- --- * @{#SET_GROUP.FilterStart}: Starts the filtering of the groups within the SET_GROUP and add or remove GROUP objects **dynamically**. --- --- Planned filter criteria within development are (so these are not yet available): --- --- * @{#SET_GROUP.FilterZones}: Builds the SET_GROUP with the groups within a @{Zone#ZONE}. --- --- ## 4. SET_GROUP iterators --- --- Once the filters have been defined and the SET_GROUP has been built, you can iterate the SET_GROUP with the available iterator methods. --- The iterator methods will walk the SET_GROUP set, and call for each element within the set a function that you provide. --- The following iterator methods are currently available within the SET_GROUP: --- --- * @{#SET_GROUP.ForEachGroup}: Calls a function for each alive group it finds within the SET_GROUP. --- * @{#SET_GROUP.ForEachGroupCompletelyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. --- * @{#SET_GROUP.ForEachGroupPartlyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Zone}, providing the GROUP and optional parameters to the called function. --- * @{#SET_GROUP.ForEachGroupNotInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. --- --- === --- @field #SET_GROUP SET_GROUP -SET_GROUP = { - ClassName = "SET_GROUP", - Filter = { - Coalitions = nil, - Categories = nil, - Countries = nil, - GroupPrefixes = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, - }, - Categories = { - plane = Group.Category.AIRPLANE, - helicopter = Group.Category.HELICOPTER, - ground = Group.Category.GROUND, -- R2.2 - ship = Group.Category.SHIP, - structure = Group.Category.STRUCTURE, - }, - }, -} - - ---- Creates a new SET_GROUP object, building a set of groups belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #SET_GROUP self --- @return #SET_GROUP --- @usage --- -- Define a new SET_GROUP Object. This DBObject will contain a reference to all alive GROUPS. --- DBObject = SET_GROUP:New() -function SET_GROUP:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.GROUPS ) ) - - return self -end - ---- Add GROUP(s) to SET_GROUP. --- @param Core.Set#SET_GROUP self --- @param #string AddGroupNames A single name or an array of GROUP names. --- @return self -function SET_GROUP:AddGroupsByName( AddGroupNames ) - - local AddGroupNamesArray = ( type( AddGroupNames ) == "table" ) and AddGroupNames or { AddGroupNames } - - for AddGroupID, AddGroupName in pairs( AddGroupNamesArray ) do - self:Add( AddGroupName, GROUP:FindByName( AddGroupName ) ) - end - - return self -end - ---- Remove GROUP(s) from SET_GROUP. --- @param Core.Set#SET_GROUP self --- @param Wrapper.Group#GROUP RemoveGroupNames A single name or an array of GROUP names. --- @return self -function SET_GROUP:RemoveGroupsByName( RemoveGroupNames ) - - local RemoveGroupNamesArray = ( type( RemoveGroupNames ) == "table" ) and RemoveGroupNames or { RemoveGroupNames } - - for RemoveGroupID, RemoveGroupName in pairs( RemoveGroupNamesArray ) do - self:Remove( RemoveGroupName.GroupName ) - end - - return self -end - - - - ---- Finds a Group based on the Group Name. --- @param #SET_GROUP self --- @param #string GroupName --- @return Wrapper.Group#GROUP The found Group. -function SET_GROUP:FindGroup( GroupName ) - - local GroupFound = self.Set[GroupName] - return GroupFound -end - ---- Iterate the SET_GROUP while identifying the nearest object from a @{Point#POINT_VEC2}. --- @param #SET_GROUP self --- @param Core.Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest object in the set. --- @return Wrapper.Group#GROUP The closest group. -function SET_GROUP:FindNearestGroupFromPointVec2( PointVec2 ) - self:F2( PointVec2 ) - - local NearestGroup = nil - local ClosestDistance = nil - - for ObjectID, ObjectData in pairs( self.Set ) do - if NearestGroup == nil then - NearestGroup = ObjectData - ClosestDistance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) - else - local Distance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) - if Distance < ClosestDistance then - NearestGroup = ObjectData - ClosestDistance = Distance - end - end - end - - return NearestGroup -end - - ---- Builds a set of groups of coalitions. --- Possible current coalitions are red, blue and neutral. --- @param #SET_GROUP self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET_GROUP self -function SET_GROUP:FilterCoalitions( Coalitions ) - if not self.Filter.Coalitions then - self.Filter.Coalitions = {} - end - if type( Coalitions ) ~= "table" then - Coalitions = { Coalitions } - end - for CoalitionID, Coalition in pairs( Coalitions ) do - self.Filter.Coalitions[Coalition] = Coalition - end - return self -end - - ---- Builds a set of groups out of categories. --- Possible current categories are plane, helicopter, ground, ship. --- @param #SET_GROUP self --- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". --- @return #SET_GROUP self -function SET_GROUP:FilterCategories( Categories ) - if not self.Filter.Categories then - self.Filter.Categories = {} - end - if type( Categories ) ~= "table" then - Categories = { Categories } - end - for CategoryID, Category in pairs( Categories ) do - self.Filter.Categories[Category] = Category - end - return self -end - ---- Builds a set of groups out of ground category. --- @param #SET_GROUP self --- @return #SET_GROUP self -function SET_GROUP:FilterCategoryGround() - self:FilterCategories( "ground" ) - return self -end - ---- Builds a set of groups out of airplane category. --- @param #SET_GROUP self --- @return #SET_GROUP self -function SET_GROUP:FilterCategoryAirplane() - self:FilterCategories( "plane" ) - return self -end - ---- Builds a set of groups out of helicopter category. --- @param #SET_GROUP self --- @return #SET_GROUP self -function SET_GROUP:FilterCategoryHelicopter() - self:FilterCategories( "helicopter" ) - return self -end - ---- Builds a set of groups out of ship category. --- @param #SET_GROUP self --- @return #SET_GROUP self -function SET_GROUP:FilterCategoryShip() - self:FilterCategories( "ship" ) - return self -end - ---- Builds a set of groups out of structure category. --- @param #SET_GROUP self --- @return #SET_GROUP self -function SET_GROUP:FilterCategoryStructure() - self:FilterCategories( "structure" ) - return self -end - - - ---- Builds a set of groups of defined countries. --- Possible current countries are those known within DCS world. --- @param #SET_GROUP self --- @param #string Countries Can take those country strings known within DCS world. --- @return #SET_GROUP self -function SET_GROUP:FilterCountries( Countries ) - if not self.Filter.Countries then - self.Filter.Countries = {} - end - if type( Countries ) ~= "table" then - Countries = { Countries } - end - for CountryID, Country in pairs( Countries ) do - self.Filter.Countries[Country] = Country - end - return self -end - - ---- Builds a set of groups of defined GROUP prefixes. --- All the groups starting with the given prefixes will be included within the set. --- @param #SET_GROUP self --- @param #string Prefixes The prefix of which the group name starts with. --- @return #SET_GROUP self -function SET_GROUP:FilterPrefixes( Prefixes ) - if not self.Filter.GroupPrefixes then - self.Filter.GroupPrefixes = {} - end - if type( Prefixes ) ~= "table" then - Prefixes = { Prefixes } - end - for PrefixID, Prefix in pairs( Prefixes ) do - self.Filter.GroupPrefixes[Prefix] = Prefix - end - return self -end - - ---- Starts the filtering. --- @param #SET_GROUP self --- @return #SET_GROUP self -function SET_GROUP:FilterStart() - - if _DATABASE then - self:_FilterStart() - end - - - - return self -end - ---- Handles the OnDead or OnCrash event for alive groups set. --- Note: The GROUP object in the SET_GROUP collection will only be removed if the last unit is destroyed of the GROUP. --- @param #SET_GROUP self --- @param Core.Event#EVENTDATA Event -function SET_GROUP:_EventOnDeadOrCrash( Event ) - self:F3( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:FindInDatabase( Event ) - if ObjectName then - if Event.IniDCSGroup:getSize() == 1 then -- Only remove if the last unit of the group was destroyed. - self:Remove( ObjectName ) - end - end - end -end - ---- Handles the Database to check on an event (birth) that the Object was added in the Database. --- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! --- @param #SET_GROUP self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the GROUP --- @return #table The GROUP -function SET_GROUP:AddInDatabase( Event ) - self:F3( { Event } ) - - if Event.IniObjectCategory == 1 then - if not self.Database[Event.IniDCSGroupName] then - self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName ) - self:T3( self.Database[Event.IniDCSGroupName] ) - end - end - - return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_GROUP self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the GROUP --- @return #table The GROUP -function SET_GROUP:FindInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] -end - ---- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP, providing the GROUP and optional parameters. --- @param #SET_GROUP self --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroup( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set ) - - return self -end - ---- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroupCompletelyInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Group#GROUP GroupObject - function( ZoneObject, GroupObject ) - if GroupObject:IsCompletelyInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Zone}, providing the GROUP and optional parameters to the called function. --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroupPartlyInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Group#GROUP GroupObject - function( ZoneObject, GroupObject ) - if GroupObject:IsPartlyInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroupNotInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Group#GROUP GroupObject - function( ZoneObject, GroupObject ) - if GroupObject:IsNotInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_GROUP and return true if all the @{Wrapper.Group#GROUP} are completely in the @{Core.Zone#ZONE} --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @return #boolean true if all the @{Wrapper.Group#GROUP} are completly in the @{Core.Zone#ZONE}, false otherwise --- @usage --- local MyZone = ZONE:New("Zone1") --- local MySetGroup = SET_GROUP:New() --- MySetGroup:AddGroupsByName({"Group1", "Group2"}) --- --- if MySetGroup:AllCompletelyInZone(MyZone) then --- MESSAGE:New("All the SET's GROUP are in zone !", 10):ToAll() --- else --- MESSAGE:New("Some or all SET's GROUP are outside zone !", 10):ToAll() --- end -function SET_GROUP:AllCompletelyInZone(Zone) - self:F2(Zone) - local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if not GroupData:IsCompletelyInZone(Zone) then - return false - end - end - return true -end - ---- Iterate the SET_GROUP and return true if at least one of the @{Wrapper.Group#GROUP} is completely inside the @{Core.Zone#ZONE} --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is completly inside the @{Core.Zone#ZONE}, false otherwise. --- @usage --- local MyZone = ZONE:New("Zone1") --- local MySetGroup = SET_GROUP:New() --- MySetGroup:AddGroupsByName({"Group1", "Group2"}) --- --- if MySetGroup:AnyCompletelyInZone(MyZone) then --- MESSAGE:New("At least one GROUP is completely in zone !", 10):ToAll() --- else --- MESSAGE:New("No GROUP is completely in zone !", 10):ToAll() --- end -function SET_GROUP:AnyCompletelyInZone(Zone) - self:F2(Zone) - local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if GroupData:IsCompletelyInZone(Zone) then - return true - end - end - return false -end - ---- Iterate the SET_GROUP and return true if at least one @{#UNIT} of one @{GROUP} of the @{SET_GROUP} is in @{ZONE} --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is partly or completly inside the @{Core.Zone#ZONE}, false otherwise. --- @usage --- local MyZone = ZONE:New("Zone1") --- local MySetGroup = SET_GROUP:New() --- MySetGroup:AddGroupsByName({"Group1", "Group2"}) --- --- if MySetGroup:AnyPartlyInZone(MyZone) then --- MESSAGE:New("At least one GROUP has at least one UNIT in zone !", 10):ToAll() --- else --- MESSAGE:New("No UNIT of any GROUP is in zone !", 10):ToAll() --- end -function SET_GROUP:AnyInZone(Zone) - self:F2(Zone) - local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if GroupData:IsPartlyInZone(Zone) or GroupData:IsCompletelyInZone(Zone) then - return true - end - end - return false -end - ---- Iterate the SET_GROUP and return true if at least one @{GROUP} of the @{SET_GROUP} is partly in @{ZONE}. --- Will return false if a @{GROUP} is fully in the @{ZONE} --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is partly or completly inside the @{Core.Zone#ZONE}, false otherwise. --- @usage --- local MyZone = ZONE:New("Zone1") --- local MySetGroup = SET_GROUP:New() --- MySetGroup:AddGroupsByName({"Group1", "Group2"}) --- --- if MySetGroup:AnyPartlyInZone(MyZone) then --- MESSAGE:New("At least one GROUP is partially in the zone, but none are fully in it !", 10):ToAll() --- else --- MESSAGE:New("No GROUP are in zone, or one (or more) GROUP is completely in it !", 10):ToAll() --- end -function SET_GROUP:AnyPartlyInZone(Zone) - self:F2(Zone) - local IsPartlyInZone = false - local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if GroupData:IsCompletelyInZone(Zone) then - return false - elseif GroupData:IsPartlyInZone(Zone) then - IsPartlyInZone = true -- at least one GROUP is partly in zone - end - end - - if IsPartlyInZone then - return true - else - return false - end -end - ---- Iterate the SET_GROUP and return true if no @{GROUP} of the @{SET_GROUP} is in @{ZONE} --- This could also be achieved with `not SET_GROUP:AnyPartlyInZone(Zone)`, but it's easier for the --- mission designer to add a dedicated method --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @return #boolean true if no @{Wrapper.Group#GROUP} is inside the @{Core.Zone#ZONE} in any way, false otherwise. --- @usage --- local MyZone = ZONE:New("Zone1") --- local MySetGroup = SET_GROUP:New() --- MySetGroup:AddGroupsByName({"Group1", "Group2"}) --- --- if MySetGroup:NoneInZone(MyZone) then --- MESSAGE:New("No GROUP is completely in zone !", 10):ToAll() --- else --- MESSAGE:New("No UNIT of any GROUP is in zone !", 10):ToAll() --- end -function SET_GROUP:NoneInZone(Zone) - self:F2(Zone) - local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if not GroupData:IsNotInZone(Zone) then -- If the GROUP is in Zone in any way - return false - end - end - return true -end - ---- Iterate the SET_GROUP and count how many GROUPs are completely in the Zone --- That could easily be done with SET_GROUP:ForEachGroupCompletelyInZone(), but this function --- provides an easy to use shortcut... --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @return #number the number of GROUPs completely in the Zone --- @usage --- local MyZone = ZONE:New("Zone1") --- local MySetGroup = SET_GROUP:New() --- MySetGroup:AddGroupsByName({"Group1", "Group2"}) --- --- MESSAGE:New("There are " .. MySetGroup:CountInZone(MyZone) .. " GROUPs in the Zone !", 10):ToAll() -function SET_GROUP:CountInZone(Zone) - self:F2(Zone) - local Count = 0 - local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if GroupData:IsCompletelyInZone(Zone) then - Count = Count + 1 - end - end - return Count -end - ---- Iterate the SET_GROUP and count how many UNITs are completely in the Zone --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @return #number the number of GROUPs completely in the Zone --- @usage --- local MyZone = ZONE:New("Zone1") --- local MySetGroup = SET_GROUP:New() --- MySetGroup:AddGroupsByName({"Group1", "Group2"}) --- --- MESSAGE:New("There are " .. MySetGroup:CountUnitInZone(MyZone) .. " UNITs in the Zone !", 10):ToAll() -function SET_GROUP:CountUnitInZone(Zone) - self:F2(Zone) - local Count = 0 - local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - Count = Count + GroupData:CountInZone(Zone) - end - return Count -end - ------ Iterate the SET_GROUP and call an interator function for each **alive** player, providing the Group of the player and optional parameters. ----- @param #SET_GROUP self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a GROUP parameter. ----- @return #SET_GROUP self ---function SET_GROUP:ForEachPlayer( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) --- --- return self ---end --- --- ------ Iterate the SET_GROUP and call an interator function for each client, providing the Client to the function and optional parameters. ----- @param #SET_GROUP self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a CLIENT parameter. ----- @return #SET_GROUP self ---function SET_GROUP:ForEachClient( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.Clients ) --- --- return self ---end - - ---- --- @param #SET_GROUP self --- @param Wrapper.Group#GROUP MooseGroup --- @return #SET_GROUP self -function SET_GROUP:IsIncludeObject( MooseGroup ) - self:F2( MooseGroup ) - local MooseGroupInclude = true - - if self.Filter.Coalitions then - local MooseGroupCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - self:T3( { "Coalition:", MooseGroup:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MooseGroup:GetCoalition() then - MooseGroupCoalition = true - end - end - MooseGroupInclude = MooseGroupInclude and MooseGroupCoalition - end - - if self.Filter.Categories then - local MooseGroupCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - self:T3( { "Category:", MooseGroup:GetCategory(), self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MooseGroup:GetCategory() then - MooseGroupCategory = true - end - end - MooseGroupInclude = MooseGroupInclude and MooseGroupCategory - end - - if self.Filter.Countries then - local MooseGroupCountry = false - for CountryID, CountryName in pairs( self.Filter.Countries ) do - self:T3( { "Country:", MooseGroup:GetCountry(), CountryName } ) - if country.id[CountryName] == MooseGroup:GetCountry() then - MooseGroupCountry = true - end - end - MooseGroupInclude = MooseGroupInclude and MooseGroupCountry - end - - if self.Filter.GroupPrefixes then - local MooseGroupPrefix = false - for GroupPrefixId, GroupPrefix in pairs( self.Filter.GroupPrefixes ) do - self:T3( { "Prefix:", string.find( MooseGroup:GetName(), GroupPrefix, 1 ), GroupPrefix } ) - if string.find( MooseGroup:GetName(), GroupPrefix:gsub ("-", "%%-"), 1 ) then - MooseGroupPrefix = true - end - end - MooseGroupInclude = MooseGroupInclude and MooseGroupPrefix - end - - self:T2( MooseGroupInclude ) - return MooseGroupInclude -end - ---- @type SET_UNIT --- @extends Core.Set#SET_BASE - ---- # 3) SET_UNIT class, extends @{Set#SET_BASE} --- --- Mission designers can use the SET_UNIT class to build sets of units belonging to certain: --- --- * Coalitions --- * Categories --- * Countries --- * Unit types --- * Starting with certain prefix strings. --- --- ## 3.1) SET_UNIT constructor --- --- Create a new SET_UNIT object with the @{#SET_UNIT.New} method: --- --- * @{#SET_UNIT.New}: Creates a new SET_UNIT object. --- --- ## 3.2) Add or Remove UNIT(s) from SET_UNIT --- --- UNITs can be added and removed using the @{Set#SET_UNIT.AddUnitsByName} and @{Set#SET_UNIT.RemoveUnitsByName} respectively. --- These methods take a single UNIT name or an array of UNIT names to be added or removed from SET_UNIT. --- --- ## 3.3) SET_UNIT filter criteria --- --- You can set filter criteria to define the set of units within the SET_UNIT. --- Filter criteria are defined by: --- --- * @{#SET_UNIT.FilterCoalitions}: Builds the SET_UNIT with the units belonging to the coalition(s). --- * @{#SET_UNIT.FilterCategories}: Builds the SET_UNIT with the units belonging to the category(ies). --- * @{#SET_UNIT.FilterTypes}: Builds the SET_UNIT with the units belonging to the unit type(s). --- * @{#SET_UNIT.FilterCountries}: Builds the SET_UNIT with the units belonging to the country(ies). --- * @{#SET_UNIT.FilterPrefixes}: Builds the SET_UNIT with the units starting with the same prefix string(s). --- --- Once the filter criteria have been set for the SET_UNIT, you can start filtering using: --- --- * @{#SET_UNIT.FilterStart}: Starts the filtering of the units within the SET_UNIT. --- --- Planned filter criteria within development are (so these are not yet available): --- --- * @{#SET_UNIT.FilterZones}: Builds the SET_UNIT with the units within a @{Zone#ZONE}. --- --- ## 3.4) SET_UNIT iterators --- --- Once the filters have been defined and the SET_UNIT has been built, you can iterate the SET_UNIT with the available iterator methods. --- The iterator methods will walk the SET_UNIT set, and call for each element within the set a function that you provide. --- The following iterator methods are currently available within the SET_UNIT: --- --- * @{#SET_UNIT.ForEachUnit}: Calls a function for each alive unit it finds within the SET_UNIT. --- * @{#SET_GROUP.ForEachGroupCompletelyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. --- * @{#SET_GROUP.ForEachGroupNotInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. --- --- Planned iterators methods in development are (so these are not yet available): --- --- * @{#SET_UNIT.ForEachUnitInUnit}: Calls a function for each unit contained within the SET_UNIT. --- * @{#SET_UNIT.ForEachUnitCompletelyInZone}: Iterate and call an iterator function for each **alive** UNIT presence completely in a @{Zone}, providing the UNIT and optional parameters to the called function. --- * @{#SET_UNIT.ForEachUnitNotInZone}: Iterate and call an iterator function for each **alive** UNIT presence not in a @{Zone}, providing the UNIT and optional parameters to the called function. --- --- ## 3.5 ) SET_UNIT atomic methods --- --- Various methods exist for a SET_UNIT to perform actions or calculations and retrieve results from the SET_UNIT: --- --- * @{#SET_UNIT.GetTypeNames}(): Retrieve the type names of the @{Unit}s in the SET, delimited by a comma. --- --- === --- @field #SET_UNIT SET_UNIT -SET_UNIT = { - ClassName = "SET_UNIT", - Units = {}, - Filter = { - Coalitions = nil, - Categories = nil, - Types = nil, - Countries = nil, - UnitPrefixes = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, - }, - Categories = { - plane = Unit.Category.AIRPLANE, - helicopter = Unit.Category.HELICOPTER, - ground = Unit.Category.GROUND_UNIT, - ship = Unit.Category.SHIP, - structure = Unit.Category.STRUCTURE, - }, - }, -} - - ---- Get the first unit from the set. --- @function [parent=#SET_UNIT] GetFirst --- @param #SET_UNIT self --- @return Wrapper.Unit#UNIT The UNIT object. - ---- Creates a new SET_UNIT object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #SET_UNIT self --- @return #SET_UNIT --- @usage --- -- Define a new SET_UNIT Object. This DBObject will contain a reference to all alive Units. --- DBObject = SET_UNIT:New() -function SET_UNIT:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.UNITS ) ) -- Core.Set#SET_UNIT - - return self -end - ---- Add UNIT(s) to SET_UNIT. --- @param #SET_UNIT self --- @param #string AddUnit A single UNIT. --- @return #SET_UNIT self -function SET_UNIT:AddUnit( AddUnit ) - self:F2( AddUnit:GetName() ) - - self:Add( AddUnit:GetName(), AddUnit ) - - return self -end - - ---- Add UNIT(s) to SET_UNIT. --- @param #SET_UNIT self --- @param #string AddUnitNames A single name or an array of UNIT names. --- @return #SET_UNIT self -function SET_UNIT:AddUnitsByName( AddUnitNames ) - - local AddUnitNamesArray = ( type( AddUnitNames ) == "table" ) and AddUnitNames or { AddUnitNames } - - self:T( AddUnitNamesArray ) - for AddUnitID, AddUnitName in pairs( AddUnitNamesArray ) do - self:Add( AddUnitName, UNIT:FindByName( AddUnitName ) ) - end - - return self -end - ---- Remove UNIT(s) from SET_UNIT. --- @param Core.Set#SET_UNIT self --- @param Wrapper.Unit#UNIT RemoveUnitNames A single name or an array of UNIT names. --- @return self -function SET_UNIT:RemoveUnitsByName( RemoveUnitNames ) - - local RemoveUnitNamesArray = ( type( RemoveUnitNames ) == "table" ) and RemoveUnitNames or { RemoveUnitNames } - - for RemoveUnitID, RemoveUnitName in pairs( RemoveUnitNamesArray ) do - self:Remove( RemoveUnitName ) - end - - return self -end - - ---- Finds a Unit based on the Unit Name. --- @param #SET_UNIT self --- @param #string UnitName --- @return Wrapper.Unit#UNIT The found Unit. -function SET_UNIT:FindUnit( UnitName ) - - local UnitFound = self.Set[UnitName] - return UnitFound -end - - - ---- Builds a set of units of coalitions. --- Possible current coalitions are red, blue and neutral. --- @param #SET_UNIT self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET_UNIT self -function SET_UNIT:FilterCoalitions( Coalitions ) - if not self.Filter.Coalitions then - self.Filter.Coalitions = {} - end - if type( Coalitions ) ~= "table" then - Coalitions = { Coalitions } - end - for CoalitionID, Coalition in pairs( Coalitions ) do - self.Filter.Coalitions[Coalition] = Coalition - end - return self -end - - ---- Builds a set of units out of categories. --- Possible current categories are plane, helicopter, ground, ship. --- @param #SET_UNIT self --- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". --- @return #SET_UNIT self -function SET_UNIT:FilterCategories( Categories ) - if not self.Filter.Categories then - self.Filter.Categories = {} - end - if type( Categories ) ~= "table" then - Categories = { Categories } - end - for CategoryID, Category in pairs( Categories ) do - self.Filter.Categories[Category] = Category - end - return self -end - - ---- Builds a set of units of defined unit types. --- Possible current types are those types known within DCS world. --- @param #SET_UNIT self --- @param #string Types Can take those type strings known within DCS world. --- @return #SET_UNIT self -function SET_UNIT:FilterTypes( Types ) - if not self.Filter.Types then - self.Filter.Types = {} - end - if type( Types ) ~= "table" then - Types = { Types } - end - for TypeID, Type in pairs( Types ) do - self.Filter.Types[Type] = Type - end - return self -end - - ---- Builds a set of units of defined countries. --- Possible current countries are those known within DCS world. --- @param #SET_UNIT self --- @param #string Countries Can take those country strings known within DCS world. --- @return #SET_UNIT self -function SET_UNIT:FilterCountries( Countries ) - if not self.Filter.Countries then - self.Filter.Countries = {} - end - if type( Countries ) ~= "table" then - Countries = { Countries } - end - for CountryID, Country in pairs( Countries ) do - self.Filter.Countries[Country] = Country - end - return self -end - - ---- Builds a set of units of defined unit prefixes. --- All the units starting with the given prefixes will be included within the set. --- @param #SET_UNIT self --- @param #string Prefixes The prefix of which the unit name starts with. --- @return #SET_UNIT self -function SET_UNIT:FilterPrefixes( Prefixes ) - if not self.Filter.UnitPrefixes then - self.Filter.UnitPrefixes = {} - end - if type( Prefixes ) ~= "table" then - Prefixes = { Prefixes } - end - for PrefixID, Prefix in pairs( Prefixes ) do - self.Filter.UnitPrefixes[Prefix] = Prefix - end - return self -end - ---- Builds a set of units having a radar of give types. --- All the units having a radar of a given type will be included within the set. --- @param #SET_UNIT self --- @param #table RadarTypes The radar types. --- @return #SET_UNIT self -function SET_UNIT:FilterHasRadar( RadarTypes ) - - self.Filter.RadarTypes = self.Filter.RadarTypes or {} - if type( RadarTypes ) ~= "table" then - RadarTypes = { RadarTypes } - end - for RadarTypeID, RadarType in pairs( RadarTypes ) do - self.Filter.RadarTypes[RadarType] = RadarType - end - return self -end - ---- Builds a set of SEADable units. --- @param #SET_UNIT self --- @return #SET_UNIT self -function SET_UNIT:FilterHasSEAD() - - self.Filter.SEAD = true - return self -end - - - ---- Starts the filtering. --- @param #SET_UNIT self --- @return #SET_UNIT self -function SET_UNIT:FilterStart() - - if _DATABASE then - self:_FilterStart() - end - - return self -end - ---- Handles the Database to check on an event (birth) that the Object was added in the Database. --- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! --- @param #SET_UNIT self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the UNIT --- @return #table The UNIT -function SET_UNIT:AddInDatabase( Event ) - self:F3( { Event } ) - - if Event.IniObjectCategory == 1 then - if not self.Database[Event.IniDCSUnitName] then - self.Database[Event.IniDCSUnitName] = UNIT:Register( Event.IniDCSUnitName ) - self:T3( self.Database[Event.IniDCSUnitName] ) - end - end - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_UNIT self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the UNIT --- @return #table The UNIT -function SET_UNIT:FindInDatabase( Event ) - self:F2( { Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName], Event } ) - - - return Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName] -end - ---- Iterate the SET_UNIT and call an interator function for each **alive** UNIT, providing the UNIT and optional parameters. --- @param #SET_UNIT self --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. --- @return #SET_UNIT self -function SET_UNIT:ForEachUnit( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set ) - - return self -end - ---- Iterate the SET_UNIT **sorted *per Threat Level** and call an interator function for each **alive** UNIT, providing the UNIT and optional parameters. --- --- @param #SET_UNIT self --- @param #number FromThreatLevel The TreatLevel to start the evaluation **From** (this must be a value between 0 and 10). --- @param #number ToThreatLevel The TreatLevel to stop the evaluation **To** (this must be a value between 0 and 10). --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. --- @return #SET_UNIT self --- @usage --- --- UnitSet:ForEachUnitPerThreatLevel( 10, 0, --- -- @param Wrapper.Unit#UNIT UnitObject The UNIT object in the UnitSet, that will be passed to the local function for evaluation. --- function( UnitObject ) --- .. logic .. --- end --- ) --- -function SET_UNIT:ForEachUnitPerThreatLevel( FromThreatLevel, ToThreatLevel, IteratorFunction, ... ) --R2.1 Threat Level implementation - self:F2( arg ) - - local ThreatLevelSet = {} - - if self:Count() ~= 0 then - for UnitName, UnitObject in pairs( self.Set ) do - local Unit = UnitObject -- Wrapper.Unit#UNIT - - local ThreatLevel = Unit:GetThreatLevel() - ThreatLevelSet[ThreatLevel] = ThreatLevelSet[ThreatLevel] or {} - ThreatLevelSet[ThreatLevel].Set = ThreatLevelSet[ThreatLevel].Set or {} - ThreatLevelSet[ThreatLevel].Set[UnitName] = UnitObject - self:E( { ThreatLevel = ThreatLevel, ThreatLevelSet = ThreatLevelSet[ThreatLevel].Set } ) - end - - local ThreatLevelIncrement = FromThreatLevel <= ToThreatLevel and 1 or -1 - - for ThreatLevel = FromThreatLevel, ToThreatLevel, ThreatLevelIncrement do - self:E( { ThreatLevel = ThreatLevel } ) - local ThreatLevelItem = ThreatLevelSet[ThreatLevel] - if ThreatLevelItem then - self:ForEach( IteratorFunction, arg, ThreatLevelItem.Set ) - end - end - end - - return self -end - - - ---- Iterate the SET_UNIT and call an iterator function for each **alive** UNIT presence completely in a @{Zone}, providing the UNIT and optional parameters to the called function. --- @param #SET_UNIT self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. --- @return #SET_UNIT self -function SET_UNIT:ForEachUnitCompletelyInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Unit#UNIT UnitObject - function( ZoneObject, UnitObject ) - if UnitObject:IsInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_UNIT and call an iterator function for each **alive** UNIT presence not in a @{Zone}, providing the UNIT and optional parameters to the called function. --- @param #SET_UNIT self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. --- @return #SET_UNIT self -function SET_UNIT:ForEachUnitNotInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Unit#UNIT UnitObject - function( ZoneObject, UnitObject ) - if UnitObject:IsNotInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Returns map of unit types. --- @param #SET_UNIT self --- @return #map<#string,#number> A map of the unit types found. The key is the UnitTypeName and the value is the amount of unit types found. -function SET_UNIT:GetUnitTypes() - self:F2() - - local MT = {} -- Message Text - local UnitTypes = {} - - for UnitID, UnitData in pairs( self:GetSet() ) do - local TextUnit = UnitData -- Wrapper.Unit#UNIT - if TextUnit:IsAlive() then - local UnitType = TextUnit:GetTypeName() - - if not UnitTypes[UnitType] then - UnitTypes[UnitType] = 1 - else - UnitTypes[UnitType] = UnitTypes[UnitType] + 1 - end - end - end - - for UnitTypeID, UnitType in pairs( UnitTypes ) do - MT[#MT+1] = UnitType .. " of " .. UnitTypeID - end - - return UnitTypes -end - - ---- Returns a comma separated string of the unit types with a count in the @{Set}. --- @param #SET_UNIT self --- @return #string The unit types string -function SET_UNIT:GetUnitTypesText() - self:F2() - - local MT = {} -- Message Text - local UnitTypes = self:GetUnitTypes() - - for UnitTypeID, UnitType in pairs( UnitTypes ) do - MT[#MT+1] = UnitType .. " of " .. UnitTypeID - end - - return table.concat( MT, ", " ) -end - ---- Returns map of unit threat levels. --- @param #SET_UNIT self --- @return #table. -function SET_UNIT:GetUnitThreatLevels() - self:F2() - - local UnitThreatLevels = {} - - for UnitID, UnitData in pairs( self:GetSet() ) do - local ThreatUnit = UnitData -- Wrapper.Unit#UNIT - if ThreatUnit:IsAlive() then - local UnitThreatLevel, UnitThreatLevelText = ThreatUnit:GetThreatLevel() - local ThreatUnitName = ThreatUnit:GetName() - - UnitThreatLevels[UnitThreatLevel] = UnitThreatLevels[UnitThreatLevel] or {} - UnitThreatLevels[UnitThreatLevel].UnitThreatLevelText = UnitThreatLevelText - UnitThreatLevels[UnitThreatLevel].Units = UnitThreatLevels[UnitThreatLevel].Units or {} - UnitThreatLevels[UnitThreatLevel].Units[ThreatUnitName] = ThreatUnit - end - end - - return UnitThreatLevels -end - ---- Calculate the maxium A2G threat level of the SET_UNIT. --- @param #SET_UNIT self --- @return #number The maximum threatlevel -function SET_UNIT:CalculateThreatLevelA2G() - - local MaxThreatLevelA2G = 0 - local MaxThreatText = "" - for UnitName, UnitData in pairs( self:GetSet() ) do - local ThreatUnit = UnitData -- Wrapper.Unit#UNIT - local ThreatLevelA2G, ThreatText = ThreatUnit:GetThreatLevel() - if ThreatLevelA2G > MaxThreatLevelA2G then - MaxThreatLevelA2G = ThreatLevelA2G - MaxThreatText = ThreatText - end - end - - self:F( { MaxThreatLevelA2G = MaxThreatLevelA2G, MaxThreatText = MaxThreatText } ) - return MaxThreatLevelA2G, MaxThreatText - -end - ---- Get the center coordinate of the SET_UNIT. --- @param #SET_UNIT self --- @return Core.Point#COORDINATE The center coordinate of all the units in the set, including heading in degrees and speed in mps in case of moving units. -function SET_UNIT:GetCoordinate() - - local Coordinate = self:GetFirst():GetCoordinate() - - local x1 = Coordinate.x - local x2 = Coordinate.x - local y1 = Coordinate.y - local y2 = Coordinate.y - local z1 = Coordinate.z - local z2 = Coordinate.z - local MaxVelocity = 0 - local AvgHeading = nil - local MovingCount = 0 - - for UnitName, UnitData in pairs( self:GetSet() ) do - - local Unit = UnitData -- Wrapper.Unit#UNIT - local Coordinate = Unit:GetCoordinate() - - x1 = ( Coordinate.x < x1 ) and Coordinate.x or x1 - x2 = ( Coordinate.x > x2 ) and Coordinate.x or x2 - y1 = ( Coordinate.y < y1 ) and Coordinate.y or y1 - y2 = ( Coordinate.y > y2 ) and Coordinate.y or y2 - z1 = ( Coordinate.y < z1 ) and Coordinate.z or z1 - z2 = ( Coordinate.y > z2 ) and Coordinate.z or z2 - - local Velocity = Coordinate:GetVelocity() - if Velocity ~= 0 then - MaxVelocity = ( MaxVelocity < Velocity ) and Velocity or MaxVelocity - local Heading = Coordinate:GetHeading() - AvgHeading = AvgHeading and ( AvgHeading + Heading ) or Heading - MovingCount = MovingCount + 1 - end - end - - AvgHeading = AvgHeading and ( AvgHeading / MovingCount ) - - Coordinate.x = ( x2 - x1 ) / 2 + x1 - Coordinate.y = ( y2 - y1 ) / 2 + y1 - Coordinate.z = ( z2 - z1 ) / 2 + z1 - Coordinate:SetHeading( AvgHeading ) - Coordinate:SetVelocity( MaxVelocity ) - - self:F( { Coordinate = Coordinate } ) - return Coordinate - -end - ---- Get the maximum velocity of the SET_UNIT. --- @param #SET_UNIT self --- @return #number The speed in mps in case of moving units. -function SET_UNIT:GetVelocity() - - local Coordinate = self:GetFirst():GetCoordinate() - - local MaxVelocity = 0 - - for UnitName, UnitData in pairs( self:GetSet() ) do - - local Unit = UnitData -- Wrapper.Unit#UNIT - local Coordinate = Unit:GetCoordinate() - - local Velocity = Coordinate:GetVelocity() - if Velocity ~= 0 then - MaxVelocity = ( MaxVelocity < Velocity ) and Velocity or MaxVelocity - end - end - - self:F( { MaxVelocity = MaxVelocity } ) - return MaxVelocity - -end - ---- Get the average heading of the SET_UNIT. --- @param #SET_UNIT self --- @return #number Heading Heading in degrees and speed in mps in case of moving units. -function SET_UNIT:GetHeading() - - local HeadingSet = nil - local MovingCount = 0 - - for UnitName, UnitData in pairs( self:GetSet() ) do - - local Unit = UnitData -- Wrapper.Unit#UNIT - local Coordinate = Unit:GetCoordinate() - - local Velocity = Coordinate:GetVelocity() - if Velocity ~= 0 then - local Heading = Coordinate:GetHeading() - if HeadingSet == nil then - HeadingSet = Heading - else - local HeadingDiff = ( HeadingSet - Heading + 180 + 360 ) % 360 - 180 - HeadingDiff = math.abs( HeadingDiff ) - if HeadingDiff > 5 then - HeadingSet = nil - break - end - end - end - end - - return HeadingSet - -end - - - ---- Returns if the @{Set} has targets having a radar (of a given type). --- @param #SET_UNIT self --- @param Dcs.DCSWrapper.Unit#Unit.RadarType RadarType --- @return #number The amount of radars in the Set with the given type -function SET_UNIT:HasRadar( RadarType ) - self:F2( RadarType ) - - local RadarCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do - local UnitSensorTest = UnitData -- Wrapper.Unit#UNIT - local HasSensors - if RadarType then - HasSensors = UnitSensorTest:HasSensors( Unit.SensorType.RADAR, RadarType ) - else - HasSensors = UnitSensorTest:HasSensors( Unit.SensorType.RADAR ) - end - self:T3(HasSensors) - if HasSensors then - RadarCount = RadarCount + 1 - end - end - - return RadarCount -end - ---- Returns if the @{Set} has targets that can be SEADed. --- @param #SET_UNIT self --- @return #number The amount of SEADable units in the Set -function SET_UNIT:HasSEAD() - self:F2() - - local SEADCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do - local UnitSEAD = UnitData -- Wrapper.Unit#UNIT - if UnitSEAD:IsAlive() then - local UnitSEADAttributes = UnitSEAD:GetDesc().attributes - - local HasSEAD = UnitSEAD:HasSEAD() - - self:T3(HasSEAD) - if HasSEAD then - SEADCount = SEADCount + 1 - end - end - end - - return SEADCount -end - ---- Returns if the @{Set} has ground targets. --- @param #SET_UNIT self --- @return #number The amount of ground targets in the Set. -function SET_UNIT:HasGroundUnits() - self:F2() - - local GroundUnitCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do - local UnitTest = UnitData -- Wrapper.Unit#UNIT - if UnitTest:IsGround() then - GroundUnitCount = GroundUnitCount + 1 - end - end - - return GroundUnitCount -end - ---- Returns if the @{Set} has friendly ground units. --- @param #SET_UNIT self --- @return #number The amount of ground targets in the Set. -function SET_UNIT:HasFriendlyUnits( FriendlyCoalition ) - self:F2() - - local FriendlyUnitCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do - local UnitTest = UnitData -- Wrapper.Unit#UNIT - if UnitTest:IsFriendly( FriendlyCoalition ) then - FriendlyUnitCount = FriendlyUnitCount + 1 - end - end - - return FriendlyUnitCount -end - - - ------ Iterate the SET_UNIT and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. ----- @param #SET_UNIT self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a UNIT parameter. ----- @return #SET_UNIT self ---function SET_UNIT:ForEachPlayer( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) --- --- return self ---end --- --- ------ Iterate the SET_UNIT and call an interator function for each client, providing the Client to the function and optional parameters. ----- @param #SET_UNIT self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a CLIENT parameter. ----- @return #SET_UNIT self ---function SET_UNIT:ForEachClient( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.Clients ) --- --- return self ---end - - ---- --- @param #SET_UNIT self --- @param Wrapper.Unit#UNIT MUnit --- @return #SET_UNIT self -function SET_UNIT:IsIncludeObject( MUnit ) - self:F2( MUnit ) - local MUnitInclude = true - - if self.Filter.Coalitions then - local MUnitCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - self:T3( { "Coalition:", MUnit:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MUnit:GetCoalition() then - MUnitCoalition = true - end - end - MUnitInclude = MUnitInclude and MUnitCoalition - end - - if self.Filter.Categories then - local MUnitCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - self:T3( { "Category:", MUnit:GetDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MUnit:GetDesc().category then - MUnitCategory = true - end - end - MUnitInclude = MUnitInclude and MUnitCategory - end - - if self.Filter.Types then - local MUnitType = false - for TypeID, TypeName in pairs( self.Filter.Types ) do - self:T3( { "Type:", MUnit:GetTypeName(), TypeName } ) - if TypeName == MUnit:GetTypeName() then - MUnitType = true - end - end - MUnitInclude = MUnitInclude and MUnitType - end - - if self.Filter.Countries then - local MUnitCountry = false - for CountryID, CountryName in pairs( self.Filter.Countries ) do - self:T3( { "Country:", MUnit:GetCountry(), CountryName } ) - if country.id[CountryName] == MUnit:GetCountry() then - MUnitCountry = true - end - end - MUnitInclude = MUnitInclude and MUnitCountry - end - - if self.Filter.UnitPrefixes then - local MUnitPrefix = false - for UnitPrefixId, UnitPrefix in pairs( self.Filter.UnitPrefixes ) do - self:T3( { "Prefix:", string.find( MUnit:GetName(), UnitPrefix, 1 ), UnitPrefix } ) - if string.find( MUnit:GetName(), UnitPrefix, 1 ) then - MUnitPrefix = true - end - end - MUnitInclude = MUnitInclude and MUnitPrefix - end - - if self.Filter.RadarTypes then - local MUnitRadar = false - for RadarTypeID, RadarType in pairs( self.Filter.RadarTypes ) do - self:T3( { "Radar:", RadarType } ) - if MUnit:HasSensors( Unit.SensorType.RADAR, RadarType ) == true then - if MUnit:GetRadar() == true then -- This call is necessary to evaluate the SEAD capability. - self:T3( "RADAR Found" ) - end - MUnitRadar = true - end - end - MUnitInclude = MUnitInclude and MUnitRadar - end - - if self.Filter.SEAD then - local MUnitSEAD = false - if MUnit:HasSEAD() == true then - self:T3( "SEAD Found" ) - MUnitSEAD = true - end - MUnitInclude = MUnitInclude and MUnitSEAD - end - - self:T2( MUnitInclude ) - return MUnitInclude -end - - ---- Retrieve the type names of the @{Unit}s in the SET, delimited by an optional delimiter. --- @param #SET_UNIT self --- @param #string Delimiter (optional) The delimiter, which is default a comma. --- @return #string The types of the @{Unit}s delimited. -function SET_UNIT:GetTypeNames( Delimiter ) - - Delimiter = Delimiter or ", " - local TypeReport = REPORT:New() - local Types = {} - - for UnitName, UnitData in pairs( self:GetSet() ) do - - local Unit = UnitData -- Wrapper.Unit#UNIT - local UnitTypeName = Unit:GetTypeName() - - if not Types[UnitTypeName] then - Types[UnitTypeName] = UnitTypeName - TypeReport:Add( UnitTypeName ) - end - end - - return TypeReport:Text( Delimiter ) -end - - ---- SET_CLIENT - - ---- @type SET_CLIENT --- @extends Core.Set#SET_BASE - - - ---- # 4) SET_CLIENT class, extends @{Set#SET_BASE} --- --- Mission designers can use the @{Set#SET_CLIENT} class to build sets of units belonging to certain: --- --- * Coalitions --- * Categories --- * Countries --- * Client types --- * Starting with certain prefix strings. --- --- ## 4.1) SET_CLIENT constructor --- --- Create a new SET_CLIENT object with the @{#SET_CLIENT.New} method: --- --- * @{#SET_CLIENT.New}: Creates a new SET_CLIENT object. --- --- ## 4.2) Add or Remove CLIENT(s) from SET_CLIENT --- --- CLIENTs can be added and removed using the @{Set#SET_CLIENT.AddClientsByName} and @{Set#SET_CLIENT.RemoveClientsByName} respectively. --- These methods take a single CLIENT name or an array of CLIENT names to be added or removed from SET_CLIENT. --- --- ## 4.3) SET_CLIENT filter criteria --- --- You can set filter criteria to define the set of clients within the SET_CLIENT. --- Filter criteria are defined by: --- --- * @{#SET_CLIENT.FilterCoalitions}: Builds the SET_CLIENT with the clients belonging to the coalition(s). --- * @{#SET_CLIENT.FilterCategories}: Builds the SET_CLIENT with the clients belonging to the category(ies). --- * @{#SET_CLIENT.FilterTypes}: Builds the SET_CLIENT with the clients belonging to the client type(s). --- * @{#SET_CLIENT.FilterCountries}: Builds the SET_CLIENT with the clients belonging to the country(ies). --- * @{#SET_CLIENT.FilterPrefixes}: Builds the SET_CLIENT with the clients starting with the same prefix string(s). --- --- Once the filter criteria have been set for the SET_CLIENT, you can start filtering using: --- --- * @{#SET_CLIENT.FilterStart}: Starts the filtering of the clients within the SET_CLIENT. --- --- Planned filter criteria within development are (so these are not yet available): --- --- * @{#SET_CLIENT.FilterZones}: Builds the SET_CLIENT with the clients within a @{Zone#ZONE}. --- --- ## 4.4) SET_CLIENT iterators --- --- Once the filters have been defined and the SET_CLIENT has been built, you can iterate the SET_CLIENT with the available iterator methods. --- The iterator methods will walk the SET_CLIENT set, and call for each element within the set a function that you provide. --- The following iterator methods are currently available within the SET_CLIENT: --- --- * @{#SET_CLIENT.ForEachClient}: Calls a function for each alive client it finds within the SET_CLIENT. --- --- === --- @field #SET_CLIENT SET_CLIENT -SET_CLIENT = { - ClassName = "SET_CLIENT", - Clients = {}, - Filter = { - Coalitions = nil, - Categories = nil, - Types = nil, - Countries = nil, - ClientPrefixes = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, - }, - Categories = { - plane = Unit.Category.AIRPLANE, - helicopter = Unit.Category.HELICOPTER, - ground = Unit.Category.GROUND_UNIT, - ship = Unit.Category.SHIP, - structure = Unit.Category.STRUCTURE, - }, - }, -} - - ---- Creates a new SET_CLIENT object, building a set of clients belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #SET_CLIENT self --- @return #SET_CLIENT --- @usage --- -- Define a new SET_CLIENT Object. This DBObject will contain a reference to all Clients. --- DBObject = SET_CLIENT:New() -function SET_CLIENT:New() - -- Inherits from BASE - local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.CLIENTS ) ) - - return self -end - ---- Add CLIENT(s) to SET_CLIENT. --- @param Core.Set#SET_CLIENT self --- @param #string AddClientNames A single name or an array of CLIENT names. --- @return self -function SET_CLIENT:AddClientsByName( AddClientNames ) - - local AddClientNamesArray = ( type( AddClientNames ) == "table" ) and AddClientNames or { AddClientNames } - - for AddClientID, AddClientName in pairs( AddClientNamesArray ) do - self:Add( AddClientName, CLIENT:FindByName( AddClientName ) ) - end - - return self -end - ---- Remove CLIENT(s) from SET_CLIENT. --- @param Core.Set#SET_CLIENT self --- @param Wrapper.Client#CLIENT RemoveClientNames A single name or an array of CLIENT names. --- @return self -function SET_CLIENT:RemoveClientsByName( RemoveClientNames ) - - local RemoveClientNamesArray = ( type( RemoveClientNames ) == "table" ) and RemoveClientNames or { RemoveClientNames } - - for RemoveClientID, RemoveClientName in pairs( RemoveClientNamesArray ) do - self:Remove( RemoveClientName.ClientName ) - end - - return self -end - - ---- Finds a Client based on the Client Name. --- @param #SET_CLIENT self --- @param #string ClientName --- @return Wrapper.Client#CLIENT The found Client. -function SET_CLIENT:FindClient( ClientName ) - - local ClientFound = self.Set[ClientName] - return ClientFound -end - - - ---- Builds a set of clients of coalitions. --- Possible current coalitions are red, blue and neutral. --- @param #SET_CLIENT self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET_CLIENT self -function SET_CLIENT:FilterCoalitions( Coalitions ) - if not self.Filter.Coalitions then - self.Filter.Coalitions = {} - end - if type( Coalitions ) ~= "table" then - Coalitions = { Coalitions } - end - for CoalitionID, Coalition in pairs( Coalitions ) do - self.Filter.Coalitions[Coalition] = Coalition - end - return self -end - - ---- Builds a set of clients out of categories. --- Possible current categories are plane, helicopter, ground, ship. --- @param #SET_CLIENT self --- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". --- @return #SET_CLIENT self -function SET_CLIENT:FilterCategories( Categories ) - if not self.Filter.Categories then - self.Filter.Categories = {} - end - if type( Categories ) ~= "table" then - Categories = { Categories } - end - for CategoryID, Category in pairs( Categories ) do - self.Filter.Categories[Category] = Category - end - return self -end - - ---- Builds a set of clients of defined client types. --- Possible current types are those types known within DCS world. --- @param #SET_CLIENT self --- @param #string Types Can take those type strings known within DCS world. --- @return #SET_CLIENT self -function SET_CLIENT:FilterTypes( Types ) - if not self.Filter.Types then - self.Filter.Types = {} - end - if type( Types ) ~= "table" then - Types = { Types } - end - for TypeID, Type in pairs( Types ) do - self.Filter.Types[Type] = Type - end - return self -end - - ---- Builds a set of clients of defined countries. --- Possible current countries are those known within DCS world. --- @param #SET_CLIENT self --- @param #string Countries Can take those country strings known within DCS world. --- @return #SET_CLIENT self -function SET_CLIENT:FilterCountries( Countries ) - if not self.Filter.Countries then - self.Filter.Countries = {} - end - if type( Countries ) ~= "table" then - Countries = { Countries } - end - for CountryID, Country in pairs( Countries ) do - self.Filter.Countries[Country] = Country - end - return self -end - - ---- Builds a set of clients of defined client prefixes. --- All the clients starting with the given prefixes will be included within the set. --- @param #SET_CLIENT self --- @param #string Prefixes The prefix of which the client name starts with. --- @return #SET_CLIENT self -function SET_CLIENT:FilterPrefixes( Prefixes ) - if not self.Filter.ClientPrefixes then - self.Filter.ClientPrefixes = {} - end - if type( Prefixes ) ~= "table" then - Prefixes = { Prefixes } - end - for PrefixID, Prefix in pairs( Prefixes ) do - self.Filter.ClientPrefixes[Prefix] = Prefix - end - return self -end - - - - ---- Starts the filtering. --- @param #SET_CLIENT self --- @return #SET_CLIENT self -function SET_CLIENT:FilterStart() - - if _DATABASE then - self:_FilterStart() - end - - return self -end - ---- Handles the Database to check on an event (birth) that the Object was added in the Database. --- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! --- @param #SET_CLIENT self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the CLIENT --- @return #table The CLIENT -function SET_CLIENT:AddInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_CLIENT self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the CLIENT --- @return #table The CLIENT -function SET_CLIENT:FindInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Iterate the SET_CLIENT and call an interator function for each **alive** CLIENT, providing the CLIENT and optional parameters. --- @param #SET_CLIENT self --- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. --- @return #SET_CLIENT self -function SET_CLIENT:ForEachClient( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set ) - - return self -end - ---- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT presence completely in a @{Zone}, providing the CLIENT and optional parameters to the called function. --- @param #SET_CLIENT self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. --- @return #SET_CLIENT self -function SET_CLIENT:ForEachClientInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Client#CLIENT ClientObject - function( ZoneObject, ClientObject ) - if ClientObject:IsInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT presence not in a @{Zone}, providing the CLIENT and optional parameters to the called function. --- @param #SET_CLIENT self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. --- @return #SET_CLIENT self -function SET_CLIENT:ForEachClientNotInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Client#CLIENT ClientObject - function( ZoneObject, ClientObject ) - if ClientObject:IsNotInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- --- @param #SET_CLIENT self --- @param Wrapper.Client#CLIENT MClient --- @return #SET_CLIENT self -function SET_CLIENT:IsIncludeObject( MClient ) - self:F2( MClient ) - - local MClientInclude = true - - if MClient then - local MClientName = MClient.UnitName - - if self.Filter.Coalitions then - local MClientCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - local ClientCoalitionID = _DATABASE:GetCoalitionFromClientTemplate( MClientName ) - self:T3( { "Coalition:", ClientCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == ClientCoalitionID then - MClientCoalition = true - end - end - self:T( { "Evaluated Coalition", MClientCoalition } ) - MClientInclude = MClientInclude and MClientCoalition - end - - if self.Filter.Categories then - local MClientCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - local ClientCategoryID = _DATABASE:GetCategoryFromClientTemplate( MClientName ) - self:T3( { "Category:", ClientCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == ClientCategoryID then - MClientCategory = true - end - end - self:T( { "Evaluated Category", MClientCategory } ) - MClientInclude = MClientInclude and MClientCategory - end - - if self.Filter.Types then - local MClientType = false - for TypeID, TypeName in pairs( self.Filter.Types ) do - self:T3( { "Type:", MClient:GetTypeName(), TypeName } ) - if TypeName == MClient:GetTypeName() then - MClientType = true - end - end - self:T( { "Evaluated Type", MClientType } ) - MClientInclude = MClientInclude and MClientType - end - - if self.Filter.Countries then - local MClientCountry = false - for CountryID, CountryName in pairs( self.Filter.Countries ) do - local ClientCountryID = _DATABASE:GetCountryFromClientTemplate(MClientName) - self:T3( { "Country:", ClientCountryID, country.id[CountryName], CountryName } ) - if country.id[CountryName] and country.id[CountryName] == ClientCountryID then - MClientCountry = true - end - end - self:T( { "Evaluated Country", MClientCountry } ) - MClientInclude = MClientInclude and MClientCountry - end - - if self.Filter.ClientPrefixes then - local MClientPrefix = false - for ClientPrefixId, ClientPrefix in pairs( self.Filter.ClientPrefixes ) do - self:T3( { "Prefix:", string.find( MClient.UnitName, ClientPrefix, 1 ), ClientPrefix } ) - if string.find( MClient.UnitName, ClientPrefix, 1 ) then - MClientPrefix = true - end - end - self:T( { "Evaluated Prefix", MClientPrefix } ) - MClientInclude = MClientInclude and MClientPrefix - end - end - - self:T2( MClientInclude ) - return MClientInclude -end - ---- @type SET_AIRBASE --- @extends Core.Set#SET_BASE - ---- # 5) SET_AIRBASE class, extends @{Set#SET_BASE} --- --- Mission designers can use the @{Set#SET_AIRBASE} class to build sets of airbases optionally belonging to certain: --- --- * Coalitions --- --- ## 5.1) SET_AIRBASE constructor --- --- Create a new SET_AIRBASE object with the @{#SET_AIRBASE.New} method: --- --- * @{#SET_AIRBASE.New}: Creates a new SET_AIRBASE object. --- --- ## 5.2) Add or Remove AIRBASEs from SET_AIRBASE --- --- AIRBASEs can be added and removed using the @{Set#SET_AIRBASE.AddAirbasesByName} and @{Set#SET_AIRBASE.RemoveAirbasesByName} respectively. --- These methods take a single AIRBASE name or an array of AIRBASE names to be added or removed from SET_AIRBASE. --- --- ## 5.3) SET_AIRBASE filter criteria --- --- You can set filter criteria to define the set of clients within the SET_AIRBASE. --- Filter criteria are defined by: --- --- * @{#SET_AIRBASE.FilterCoalitions}: Builds the SET_AIRBASE with the airbases belonging to the coalition(s). --- --- Once the filter criteria have been set for the SET_AIRBASE, you can start filtering using: --- --- * @{#SET_AIRBASE.FilterStart}: Starts the filtering of the airbases within the SET_AIRBASE. --- --- ## 5.4) SET_AIRBASE iterators --- --- Once the filters have been defined and the SET_AIRBASE has been built, you can iterate the SET_AIRBASE with the available iterator methods. --- The iterator methods will walk the SET_AIRBASE set, and call for each airbase within the set a function that you provide. --- The following iterator methods are currently available within the SET_AIRBASE: --- --- * @{#SET_AIRBASE.ForEachAirbase}: Calls a function for each airbase it finds within the SET_AIRBASE. --- --- === --- @field #SET_AIRBASE SET_AIRBASE -SET_AIRBASE = { - ClassName = "SET_AIRBASE", - Airbases = {}, - Filter = { - Coalitions = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, - }, - Categories = { - airdrome = Airbase.Category.AIRDROME, - helipad = Airbase.Category.HELIPAD, - ship = Airbase.Category.SHIP, - }, - }, -} - - ---- Creates a new SET_AIRBASE object, building a set of airbases belonging to a coalitions and categories. --- @param #SET_AIRBASE self --- @return #SET_AIRBASE self --- @usage --- -- Define a new SET_AIRBASE Object. The DatabaseSet will contain a reference to all Airbases. --- DatabaseSet = SET_AIRBASE:New() -function SET_AIRBASE:New() - -- Inherits from BASE - local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.AIRBASES ) ) - - return self -end - ---- Add AIRBASEs to SET_AIRBASE. --- @param Core.Set#SET_AIRBASE self --- @param #string AddAirbaseNames A single name or an array of AIRBASE names. --- @return self -function SET_AIRBASE:AddAirbasesByName( AddAirbaseNames ) - - local AddAirbaseNamesArray = ( type( AddAirbaseNames ) == "table" ) and AddAirbaseNames or { AddAirbaseNames } - - for AddAirbaseID, AddAirbaseName in pairs( AddAirbaseNamesArray ) do - self:Add( AddAirbaseName, AIRBASE:FindByName( AddAirbaseName ) ) - end - - return self -end - ---- Remove AIRBASEs from SET_AIRBASE. --- @param Core.Set#SET_AIRBASE self --- @param Wrapper.Airbase#AIRBASE RemoveAirbaseNames A single name or an array of AIRBASE names. --- @return self -function SET_AIRBASE:RemoveAirbasesByName( RemoveAirbaseNames ) - - local RemoveAirbaseNamesArray = ( type( RemoveAirbaseNames ) == "table" ) and RemoveAirbaseNames or { RemoveAirbaseNames } - - for RemoveAirbaseID, RemoveAirbaseName in pairs( RemoveAirbaseNamesArray ) do - self:Remove( RemoveAirbaseName.AirbaseName ) - end - - return self -end - - ---- Finds a Airbase based on the Airbase Name. --- @param #SET_AIRBASE self --- @param #string AirbaseName --- @return Wrapper.Airbase#AIRBASE The found Airbase. -function SET_AIRBASE:FindAirbase( AirbaseName ) - - local AirbaseFound = self.Set[AirbaseName] - return AirbaseFound -end - - - ---- Builds a set of airbases of coalitions. --- Possible current coalitions are red, blue and neutral. --- @param #SET_AIRBASE self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET_AIRBASE self -function SET_AIRBASE:FilterCoalitions( Coalitions ) - if not self.Filter.Coalitions then - self.Filter.Coalitions = {} - end - if type( Coalitions ) ~= "table" then - Coalitions = { Coalitions } - end - for CoalitionID, Coalition in pairs( Coalitions ) do - self.Filter.Coalitions[Coalition] = Coalition - end - return self -end - - ---- Builds a set of airbases out of categories. --- Possible current categories are plane, helicopter, ground, ship. --- @param #SET_AIRBASE self --- @param #string Categories Can take the following values: "airdrome", "helipad", "ship". --- @return #SET_AIRBASE self -function SET_AIRBASE:FilterCategories( Categories ) - if not self.Filter.Categories then - self.Filter.Categories = {} - end - if type( Categories ) ~= "table" then - Categories = { Categories } - end - for CategoryID, Category in pairs( Categories ) do - self.Filter.Categories[Category] = Category - end - return self -end - ---- Starts the filtering. --- @param #SET_AIRBASE self --- @return #SET_AIRBASE self -function SET_AIRBASE:FilterStart() - - if _DATABASE then - self:_FilterStart() - end - - return self -end - - ---- Handles the Database to check on an event (birth) that the Object was added in the Database. --- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! --- @param #SET_AIRBASE self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the AIRBASE --- @return #table The AIRBASE -function SET_AIRBASE:AddInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_AIRBASE self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the AIRBASE --- @return #table The AIRBASE -function SET_AIRBASE:FindInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Iterate the SET_AIRBASE and call an interator function for each AIRBASE, providing the AIRBASE and optional parameters. --- @param #SET_AIRBASE self --- @param #function IteratorFunction The function that will be called when there is an alive AIRBASE in the SET_AIRBASE. The function needs to accept a AIRBASE parameter. --- @return #SET_AIRBASE self -function SET_AIRBASE:ForEachAirbase( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set ) - - return self -end - ---- Iterate the SET_AIRBASE while identifying the nearest @{Airbase#AIRBASE} from a @{Point#POINT_VEC2}. --- @param #SET_AIRBASE self --- @param Core.Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest @{Airbase#AIRBASE}. --- @return Wrapper.Airbase#AIRBASE The closest @{Airbase#AIRBASE}. -function SET_AIRBASE:FindNearestAirbaseFromPointVec2( PointVec2 ) - self:F2( PointVec2 ) - - local NearestAirbase = self:FindNearestObjectFromPointVec2( PointVec2 ) - return NearestAirbase -end - - - ---- --- @param #SET_AIRBASE self --- @param Wrapper.Airbase#AIRBASE MAirbase --- @return #SET_AIRBASE self -function SET_AIRBASE:IsIncludeObject( MAirbase ) - self:F2( MAirbase ) - - local MAirbaseInclude = true - - if MAirbase then - local MAirbaseName = MAirbase:GetName() - - if self.Filter.Coalitions then - local MAirbaseCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - local AirbaseCoalitionID = _DATABASE:GetCoalitionFromAirbase( MAirbaseName ) - self:T3( { "Coalition:", AirbaseCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == AirbaseCoalitionID then - MAirbaseCoalition = true - end - end - self:T( { "Evaluated Coalition", MAirbaseCoalition } ) - MAirbaseInclude = MAirbaseInclude and MAirbaseCoalition - end - - if self.Filter.Categories then - local MAirbaseCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - local AirbaseCategoryID = _DATABASE:GetCategoryFromAirbase( MAirbaseName ) - self:T3( { "Category:", AirbaseCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == AirbaseCategoryID then - MAirbaseCategory = true - end - end - self:T( { "Evaluated Category", MAirbaseCategory } ) - MAirbaseInclude = MAirbaseInclude and MAirbaseCategory - end - end - - self:T2( MAirbaseInclude ) - return MAirbaseInclude -end - ---- @type SET_CARGO --- @extends Core.Set#SET_BASE - ---- # (R2.1) SET_CARGO class, extends @{Set#SET_BASE} --- --- Mission designers can use the @{Set#SET_CARGO} class to build sets of cargos optionally belonging to certain: --- --- * Coalitions --- * Types --- * Name or Prefix --- --- ## SET_CARGO constructor --- --- Create a new SET_CARGO object with the @{#SET_CARGO.New} method: --- --- * @{#SET_CARGO.New}: Creates a new SET_CARGO object. --- --- ## Add or Remove CARGOs from SET_CARGO --- --- CARGOs can be added and removed using the @{Set#SET_CARGO.AddCargosByName} and @{Set#SET_CARGO.RemoveCargosByName} respectively. --- These methods take a single CARGO name or an array of CARGO names to be added or removed from SET_CARGO. --- --- ## SET_CARGO filter criteria --- --- You can set filter criteria to automatically maintain the SET_CARGO contents. --- Filter criteria are defined by: --- --- * @{#SET_CARGO.FilterCoalitions}: Builds the SET_CARGO with the cargos belonging to the coalition(s). --- * @{#SET_CARGO.FilterPrefixes}: Builds the SET_CARGO with the cargos containing the prefix string(s). --- * @{#SET_CARGO.FilterTypes}: Builds the SET_CARGO with the cargos belonging to the cargo type(s). --- * @{#SET_CARGO.FilterCountries}: Builds the SET_CARGO with the cargos belonging to the country(ies). --- --- Once the filter criteria have been set for the SET_CARGO, you can start filtering using: --- --- * @{#SET_CARGO.FilterStart}: Starts the filtering of the cargos within the SET_CARGO. --- --- ## SET_CARGO iterators --- --- Once the filters have been defined and the SET_CARGO has been built, you can iterate the SET_CARGO with the available iterator methods. --- The iterator methods will walk the SET_CARGO set, and call for each cargo within the set a function that you provide. --- The following iterator methods are currently available within the SET_CARGO: --- --- * @{#SET_CARGO.ForEachCargo}: Calls a function for each cargo it finds within the SET_CARGO. --- --- @field #SET_CARGO SET_CARGO --- -SET_CARGO = { - ClassName = "SET_CARGO", - Cargos = {}, - Filter = { - Coalitions = nil, - Types = nil, - Countries = nil, - ClientPrefixes = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, - }, - }, -} - - ---- (R2.1) Creates a new SET_CARGO object, building a set of cargos belonging to a coalitions and categories. --- @param #SET_CARGO self --- @return #SET_CARGO --- @usage --- -- Define a new SET_CARGO Object. The DatabaseSet will contain a reference to all Cargos. --- DatabaseSet = SET_CARGO:New() -function SET_CARGO:New() --R2.1 - -- Inherits from BASE - local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.CARGOS ) ) -- #SET_CARGO - - return self -end - ---- (R2.1) Add CARGOs to SET_CARGO. --- @param Core.Set#SET_CARGO self --- @param #string AddCargoNames A single name or an array of CARGO names. --- @return self -function SET_CARGO:AddCargosByName( AddCargoNames ) --R2.1 - - local AddCargoNamesArray = ( type( AddCargoNames ) == "table" ) and AddCargoNames or { AddCargoNames } - - for AddCargoID, AddCargoName in pairs( AddCargoNamesArray ) do - self:Add( AddCargoName, CARGO:FindByName( AddCargoName ) ) - end - - return self -end - ---- (R2.1) Remove CARGOs from SET_CARGO. --- @param Core.Set#SET_CARGO self --- @param Wrapper.Cargo#CARGO RemoveCargoNames A single name or an array of CARGO names. --- @return self -function SET_CARGO:RemoveCargosByName( RemoveCargoNames ) --R2.1 - - local RemoveCargoNamesArray = ( type( RemoveCargoNames ) == "table" ) and RemoveCargoNames or { RemoveCargoNames } - - for RemoveCargoID, RemoveCargoName in pairs( RemoveCargoNamesArray ) do - self:Remove( RemoveCargoName.CargoName ) - end - - return self -end - - ---- (R2.1) Finds a Cargo based on the Cargo Name. --- @param #SET_CARGO self --- @param #string CargoName --- @return Wrapper.Cargo#CARGO The found Cargo. -function SET_CARGO:FindCargo( CargoName ) --R2.1 - - local CargoFound = self.Set[CargoName] - return CargoFound -end - - - ---- (R2.1) Builds a set of cargos of coalitions. --- Possible current coalitions are red, blue and neutral. --- @param #SET_CARGO self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET_CARGO self -function SET_CARGO:FilterCoalitions( Coalitions ) --R2.1 - if not self.Filter.Coalitions then - self.Filter.Coalitions = {} - end - if type( Coalitions ) ~= "table" then - Coalitions = { Coalitions } - end - for CoalitionID, Coalition in pairs( Coalitions ) do - self.Filter.Coalitions[Coalition] = Coalition - end - return self -end - ---- (R2.1) Builds a set of cargos of defined cargo types. --- Possible current types are those types known within DCS world. --- @param #SET_CARGO self --- @param #string Types Can take those type strings known within DCS world. --- @return #SET_CARGO self -function SET_CARGO:FilterTypes( Types ) --R2.1 - if not self.Filter.Types then - self.Filter.Types = {} - end - if type( Types ) ~= "table" then - Types = { Types } - end - for TypeID, Type in pairs( Types ) do - self.Filter.Types[Type] = Type - end - return self -end - - ---- (R2.1) Builds a set of cargos of defined countries. --- Possible current countries are those known within DCS world. --- @param #SET_CARGO self --- @param #string Countries Can take those country strings known within DCS world. --- @return #SET_CARGO self -function SET_CARGO:FilterCountries( Countries ) --R2.1 - if not self.Filter.Countries then - self.Filter.Countries = {} - end - if type( Countries ) ~= "table" then - Countries = { Countries } - end - for CountryID, Country in pairs( Countries ) do - self.Filter.Countries[Country] = Country - end - return self -end - - ---- (R2.1) Builds a set of cargos of defined cargo prefixes. --- All the cargos starting with the given prefixes will be included within the set. --- @param #SET_CARGO self --- @param #string Prefixes The prefix of which the cargo name starts with. --- @return #SET_CARGO self -function SET_CARGO:FilterPrefixes( Prefixes ) --R2.1 - if not self.Filter.CargoPrefixes then - self.Filter.CargoPrefixes = {} - end - if type( Prefixes ) ~= "table" then - Prefixes = { Prefixes } - end - for PrefixID, Prefix in pairs( Prefixes ) do - self.Filter.CargoPrefixes[Prefix] = Prefix - end - return self -end - - - ---- (R2.1) Starts the filtering. --- @param #SET_CARGO self --- @return #SET_CARGO self -function SET_CARGO:FilterStart() --R2.1 - - if _DATABASE then - self:_FilterStart() - end - - self:HandleEvent( EVENTS.NewCargo ) - self:HandleEvent( EVENTS.DeleteCargo ) - - return self -end - - ---- (R2.1) Handles the Database to check on an event (birth) that the Object was added in the Database. --- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! --- @param #SET_CARGO self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the CARGO --- @return #table The CARGO -function SET_CARGO:AddInDatabase( Event ) --R2.1 - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- (R2.1) Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_CARGO self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the CARGO --- @return #table The CARGO -function SET_CARGO:FindInDatabase( Event ) --R2.1 - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- (R2.1) Iterate the SET_CARGO and call an interator function for each CARGO, providing the CARGO and optional parameters. --- @param #SET_CARGO self --- @param #function IteratorFunction The function that will be called when there is an alive CARGO in the SET_CARGO. The function needs to accept a CARGO parameter. --- @return #SET_CARGO self -function SET_CARGO:ForEachCargo( IteratorFunction, ... ) --R2.1 - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set ) - - return self -end - ---- (R2.1) Iterate the SET_CARGO while identifying the nearest @{Cargo#CARGO} from a @{Point#POINT_VEC2}. --- @param #SET_CARGO self --- @param Core.Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest @{Cargo#CARGO}. --- @return Wrapper.Cargo#CARGO The closest @{Cargo#CARGO}. -function SET_CARGO:FindNearestCargoFromPointVec2( PointVec2 ) --R2.1 - self:F2( PointVec2 ) - - local NearestCargo = self:FindNearestObjectFromPointVec2( PointVec2 ) - return NearestCargo -end - - - ---- (R2.1) --- @param #SET_CARGO self --- @param AI.AI_Cargo#AI_CARGO MCargo --- @return #SET_CARGO self -function SET_CARGO:IsIncludeObject( MCargo ) --R2.1 - self:F2( MCargo ) - - local MCargoInclude = true - - if MCargo then - local MCargoName = MCargo:GetName() - - if self.Filter.Coalitions then - local MCargoCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - local CargoCoalitionID = MCargo:GetCoalition() - self:T3( { "Coalition:", CargoCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == CargoCoalitionID then - MCargoCoalition = true - end - end - self:T( { "Evaluated Coalition", MCargoCoalition } ) - MCargoInclude = MCargoInclude and MCargoCoalition - end - - if self.Filter.Types then - local MCargoType = false - for TypeID, TypeName in pairs( self.Filter.Types ) do - self:T3( { "Type:", MCargo:GetType(), TypeName } ) - if TypeName == MCargo:GetType() then - MCargoType = true - end - end - self:T( { "Evaluated Type", MCargoType } ) - MCargoInclude = MCargoInclude and MCargoType - end - - if self.Filter.CargoPrefixes then - local MCargoPrefix = false - for CargoPrefixId, CargoPrefix in pairs( self.Filter.CargoPrefixes ) do - self:T3( { "Prefix:", string.find( MCargo.Name, CargoPrefix, 1 ), CargoPrefix } ) - if string.find( MCargo.Name, CargoPrefix, 1 ) then - MCargoPrefix = true - end - end - self:T( { "Evaluated Prefix", MCargoPrefix } ) - MCargoInclude = MCargoInclude and MCargoPrefix - end - end - - self:T2( MCargoInclude ) - return MCargoInclude -end - ---- (R2.1) Handles the OnEventNewCargo event for the Set. --- @param #SET_CARGO self --- @param Core.Event#EVENTDATA EventData -function SET_CARGO:OnEventNewCargo( EventData ) --R2.1 - - if EventData.Cargo then - if EventData.Cargo and self:IsIncludeObject( EventData.Cargo ) then - self:Add( EventData.Cargo.Name , EventData.Cargo ) - end - end -end - ---- (R2.1) Handles the OnDead or OnCrash event for alive units set. --- @param #SET_CARGO self --- @param Core.Event#EVENTDATA EventData -function SET_CARGO:OnEventDeleteCargo( EventData ) --R2.1 - self:F3( { EventData } ) - - if EventData.Cargo then - local Cargo = _DATABASE:FindCargo( EventData.Cargo.Name ) - if Cargo and Cargo.Name then - self:Remove( Cargo.Name ) - end - end -end - ---- **Core** -- **POINT\_VEC** classes define an **extensive API** to **manage 3D points** in the simulation space. --- --- ![Banner Image](..\Presentations\POINT\Dia1.JPG) --- --- ==== --- --- # Demo Missions --- --- ### [POINT_VEC Demo Missions source code]() --- --- ### [POINT_VEC Demo Missions, only for beta testers]() --- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) --- --- ==== --- --- # YouTube Channel --- --- ### [POINT_VEC YouTube Channel]() --- --- === --- --- ### Authors: --- --- * FlightControl : Design & Programming --- --- ### Contributions: --- --- @module Point - - - - - -do -- COORDINATE - - --- @type COORDINATE - -- @extends Core.Base#BASE - - - --- # COORDINATE class, extends @{Base#BASE} - -- - -- COORDINATE defines a 3D point in the simulator and with its methods, you can use or manipulate the point in 3D space. - -- - -- ## COORDINATE constructor - -- - -- A new COORDINATE object can be created with: - -- - -- * @{#COORDINATE.New}(): a 3D point. - -- * @{#COORDINATE.NewFromVec2}(): a 2D point created from a @{DCSTypes#Vec2}. - -- * @{#COORDINATE.NewFromVec3}(): a 3D point created from a @{DCSTypes#Vec3}. - -- - -- ## Create waypoints for routes - -- - -- A COORDINATE can prepare waypoints for Ground and Air groups to be embedded into a Route. - -- - -- * @{#COORDINATE.WaypointAir}(): Build an air route point. - -- * @{#COORDINATE.WaypointGround}(): Build a ground route point. - -- - -- Route points can be used in the Route methods of the @{Group#GROUP} class. - -- - -- - -- ## Smoke, flare, explode, illuminate - -- - -- At the point a smoke, flare, explosion and illumination bomb can be triggered. Use the following methods: - -- - -- ### Smoke - -- - -- * @{#COORDINATE.Smoke}(): To smoke the point in a certain color. - -- * @{#COORDINATE.SmokeBlue}(): To smoke the point in blue. - -- * @{#COORDINATE.SmokeRed}(): To smoke the point in red. - -- * @{#COORDINATE.SmokeOrange}(): To smoke the point in orange. - -- * @{#COORDINATE.SmokeWhite}(): To smoke the point in white. - -- * @{#COORDINATE.SmokeGreen}(): To smoke the point in green. - -- - -- ### Flare - -- - -- * @{#COORDINATE.Flare}(): To flare the point in a certain color. - -- * @{#COORDINATE.FlareRed}(): To flare the point in red. - -- * @{#COORDINATE.FlareYellow}(): To flare the point in yellow. - -- * @{#COORDINATE.FlareWhite}(): To flare the point in white. - -- * @{#COORDINATE.FlareGreen}(): To flare the point in green. - -- - -- ### Explode - -- - -- * @{#COORDINATE.Explosion}(): To explode the point with a certain intensity. - -- - -- ### Illuminate - -- - -- * @{#COORDINATE.IlluminationBomb}(): To illuminate the point. - -- - -- - -- ## Markings - -- - -- Place markers (text boxes with clarifications for briefings, target locations or any other reference point) on the map for all players, coalitions or specific groups: - -- - -- * @{#COORDINATE.MarkToAll}(): Place a mark to all players. - -- * @{#COORDINATE.MarkToCoalition}(): Place a mark to a coalition. - -- * @{#COORDINATE.MarkToCoalitionRed}(): Place a mark to the red coalition. - -- * @{#COORDINATE.MarkToCoalitionBlue}(): Place a mark to the blue coalition. - -- * @{#COORDINATE.MarkToGroup}(): Place a mark to a group (needs to have a client in it or a CA group (CA group is bugged)). - -- * @{#COORDINATE.RemoveMark}(): Removes a mark from the map. - -- - -- - -- ## 3D calculation methods - -- - -- Various calculation methods exist to use or manipulate 3D space. Find below a short description of each method: - -- - -- ### Distance - -- - -- * @{#COORDINATE.Get3DDistance}(): Obtain the distance from the current 3D point to the provided 3D point in 3D space. - -- * @{#COORDINATE.Get2DDistance}(): Obtain the distance from the current 3D point to the provided 3D point in 2D space. - -- - -- ### Angle - -- - -- * @{#COORDINATE.GetAngleDegrees}(): Obtain the angle in degrees from the current 3D point with the provided 3D direction vector. - -- * @{#COORDINATE.GetAngleRadians}(): Obtain the angle in radians from the current 3D point with the provided 3D direction vector. - -- * @{#COORDINATE.GetDirectionVec3}(): Obtain the 3D direction vector from the current 3D point to the provided 3D point. - -- - -- ### Translation - -- - -- * @{#COORDINATE.Translate}(): Translate the current 3D point towards an other 3D point using the given Distance and Angle. - -- - -- ### Get the North correction of the current location - -- - -- * @{#COORDINATE.GetNorthCorrection}(): Obtains the north correction at the current 3D point. - -- - -- - -- ## Point Randomization - -- - -- Various methods exist to calculate random locations around a given 3D point. - -- - -- * @{#COORDINATE.GetRandomVec2InRadius}(): Provides a random 2D vector around the current 3D point, in the given inner to outer band. - -- * @{#COORDINATE.GetRandomVec3InRadius}(): Provides a random 3D vector around the current 3D point, in the given inner to outer band. - -- - -- - -- ## Metric system - -- - -- * @{#COORDINATE.IsMetric}(): Returns if the 3D point is Metric or Nautical Miles. - -- * @{#COORDINATE.SetMetric}(): Sets the 3D point to Metric or Nautical Miles. - -- - -- - -- ## Coorinate text generation - -- - -- * @{#COORDINATE.ToStringBR}(): Generates a Bearing & Range text in the format of DDD for DI where DDD is degrees and DI is distance. - -- * @{#COORDINATE.ToStringLL}(): Generates a Latutude & Longutude text. - -- - -- @field #COORDINATE - COORDINATE = { - ClassName = "COORDINATE", - } - - - --- COORDINATE constructor. - -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. - -- @param Dcs.DCSTypes#Distance y The y coordinate of the Vec3 point, pointing to the Right. - -- @param Dcs.DCSTypes#Distance z The z coordinate of the Vec3 point, pointing to the Right. - -- @return #COORDINATE - function COORDINATE:New( x, y, z ) - - local self = BASE:Inherit( self, BASE:New() ) -- #COORDINATE - self.x = x - self.y = y - self.z = z - - return self - end - - --- Create a new COORDINATE object from Vec2 coordinates. - -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 point. - -- @param Dcs.DCSTypes#Distance LandHeightAdd (optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. - -- @return #COORDINATE - function COORDINATE:NewFromVec2( Vec2, LandHeightAdd ) - - local LandHeight = land.getHeight( Vec2 ) - - LandHeightAdd = LandHeightAdd or 0 - LandHeight = LandHeight + LandHeightAdd - - local self = self:New( Vec2.x, LandHeight, Vec2.y ) -- #COORDINATE - - self:F2( self ) - - return self - - end - - --- Create a new COORDINATE object from Vec3 coordinates. - -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 point. - -- @return #COORDINATE - function COORDINATE:NewFromVec3( Vec3 ) - - local self = self:New( Vec3.x, Vec3.y, Vec3.z ) -- #COORDINATE - - self:F2( self ) - - return self - end - - - --- Return the coordinates of the COORDINATE in Vec3 format. - -- @param #COORDINATE self - -- @return Dcs.DCSTypes#Vec3 The Vec3 format coordinate. - function COORDINATE:GetVec3() - return { x = self.x, y = self.y, z = self.z } - end - - - --- Return the coordinates of the COORDINATE in Vec2 format. - -- @param #COORDINATE self - -- @return Dcs.DCSTypes#Vec2 The Vec2 format coordinate. - function COORDINATE:GetVec2() - return { x = self.x, y = self.z } - end - - --TODO: check this to replace - --- Calculate the distance from a reference @{DCSTypes#Vec2}. - -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Vec2 Vec2Reference The reference @{DCSTypes#Vec2}. - -- @return Dcs.DCSTypes#Distance The distance from the reference @{DCSTypes#Vec2} in meters. - function COORDINATE:DistanceFromVec2( Vec2Reference ) - self:F2( Vec2Reference ) - - local Distance = ( ( Vec2Reference.x - self.x ) ^ 2 + ( Vec2Reference.y - self.z ) ^2 ) ^0.5 - - self:T2( Distance ) - return Distance - end - - - --- Add a Distance in meters from the COORDINATE orthonormal plane, with the given angle, and calculate the new COORDINATE. - -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Distance Distance The Distance to be added in meters. - -- @param Dcs.DCSTypes#Angle Angle The Angle in degrees. - -- @return #COORDINATE The new calculated COORDINATE. - function COORDINATE:Translate( Distance, Angle ) - local SX = self.x - local SY = self.z - local Radians = Angle / 180 * math.pi - local TX = Distance * math.cos( Radians ) + SX - local TY = Distance * math.sin( Radians ) + SY - - return COORDINATE:NewFromVec2( { x = TX, y = TY } ) - end - - --- Return a random Vec2 within an Outer Radius and optionally NOT within an Inner Radius of the COORDINATE. - -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Distance OuterRadius - -- @param Dcs.DCSTypes#Distance InnerRadius - -- @return Dcs.DCSTypes#Vec2 Vec2 - function COORDINATE:GetRandomVec2InRadius( OuterRadius, InnerRadius ) - self:F2( { OuterRadius, InnerRadius } ) - - local Theta = 2 * math.pi * math.random() - local Radials = math.random() + math.random() - if Radials > 1 then - Radials = 2 - Radials - end - - local RadialMultiplier - if InnerRadius and InnerRadius <= OuterRadius then - RadialMultiplier = ( OuterRadius - InnerRadius ) * Radials + InnerRadius - else - RadialMultiplier = OuterRadius * Radials - end - - local RandomVec2 - if OuterRadius > 0 then - RandomVec2 = { x = math.cos( Theta ) * RadialMultiplier + self.x, y = math.sin( Theta ) * RadialMultiplier + self.z } - else - RandomVec2 = { x = self.x, y = self.z } - end - - return RandomVec2 - end - - - --- Return a random Vec3 within an Outer Radius and optionally NOT within an Inner Radius of the COORDINATE. - -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Distance OuterRadius - -- @param Dcs.DCSTypes#Distance InnerRadius - -- @return Dcs.DCSTypes#Vec3 Vec3 - function COORDINATE:GetRandomVec3InRadius( OuterRadius, InnerRadius ) - - local RandomVec2 = self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) - local y = self.y + math.random( InnerRadius, OuterRadius ) - local RandomVec3 = { x = RandomVec2.x, y = y, z = RandomVec2.y } - - return RandomVec3 - end - - --- Return the height of the land at the coordinate. - -- @param #COORDINATE self - -- @return #number - function COORDINATE:GetLandHeight() - local Vec2 = { x = self.x, y = self.z } - return land.getHeight( Vec2 ) - end - - - --- Set the heading of the coordinate, if applicable. - -- @param #COORDINATE self - function COORDINATE:SetHeading( Heading ) - self.Heading = Heading - end - - - --- Get the heading of the coordinate, if applicable. - -- @param #COORDINATE self - -- @return #number or nil - function COORDINATE:GetHeading() - return self.Heading - end - - - --- Set the velocity of the COORDINATE. - -- @param #COORDINATE self - -- @param #string Velocity Velocity in meters per second. - function COORDINATE:SetVelocity( Velocity ) - self.Velocity = Velocity - end - - - --- Return the velocity of the COORDINATE. - -- @param #COORDINATE self - -- @return #number Velocity in meters per second. - function COORDINATE:GetVelocity() - local Velocity = self.Velocity - return Velocity or 0 - end - - - --- Return velocity text of the COORDINATE. - -- @param #COORDINATE self - -- @return #string - function COORDINATE:GetMovingText( Settings ) - - return self:GetVelocityText( Settings ) .. ", " .. self:GetHeadingText( Settings ) - end - - - --- Return a direction vector Vec3 from COORDINATE to the COORDINATE. - -- @param #COORDINATE self - -- @param #COORDINATE TargetCoordinate The target COORDINATE. - -- @return Dcs.DCSTypes#Vec3 DirectionVec3 The direction vector in Vec3 format. - function COORDINATE:GetDirectionVec3( TargetCoordinate ) - return { x = TargetCoordinate.x - self.x, y = TargetCoordinate.y - self.y, z = TargetCoordinate.z - self.z } - end - - - --- Get a correction in radians of the real magnetic north of the COORDINATE. - -- @param #COORDINATE self - -- @return #number CorrectionRadians The correction in radians. - function COORDINATE:GetNorthCorrectionRadians() - local TargetVec3 = self:GetVec3() - local lat, lon = coord.LOtoLL(TargetVec3) - local north_posit = coord.LLtoLO(lat + 1, lon) - return math.atan2( north_posit.z - TargetVec3.z, north_posit.x - TargetVec3.x ) - end - - - --- Return an angle in radians from the COORDINATE using a direction vector in Vec3 format. - -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Vec3 DirectionVec3 The direction vector in Vec3 format. - -- @return #number DirectionRadians The angle in radians. - function COORDINATE:GetAngleRadians( DirectionVec3 ) - local DirectionRadians = math.atan2( DirectionVec3.z, DirectionVec3.x ) - --DirectionRadians = DirectionRadians + self:GetNorthCorrectionRadians() - if DirectionRadians < 0 then - DirectionRadians = DirectionRadians + 2 * math.pi -- put dir in range of 0 to 2*pi ( the full circle ) - end - return DirectionRadians - end - - --- Return an angle in degrees from the COORDINATE using a direction vector in Vec3 format. - -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Vec3 DirectionVec3 The direction vector in Vec3 format. - -- @return #number DirectionRadians The angle in degrees. - function COORDINATE:GetAngleDegrees( DirectionVec3 ) - local AngleRadians = self:GetAngleRadians( DirectionVec3 ) - local Angle = UTILS.ToDegree( AngleRadians ) - return Angle - end - - - --- Return the 2D distance in meters between the target COORDINATE and the COORDINATE. - -- @param #COORDINATE self - -- @param #COORDINATE TargetCoordinate The target COORDINATE. - -- @return Dcs.DCSTypes#Distance Distance The distance in meters. - function COORDINATE:Get2DDistance( TargetCoordinate ) - local TargetVec3 = TargetCoordinate:GetVec3() - local SourceVec3 = self:GetVec3() - return ( ( TargetVec3.x - SourceVec3.x ) ^ 2 + ( TargetVec3.z - SourceVec3.z ) ^ 2 ) ^ 0.5 - end - - - --- Return the 3D distance in meters between the target COORDINATE and the COORDINATE. - -- @param #COORDINATE self - -- @param #COORDINATE TargetCoordinate The target COORDINATE. - -- @return Dcs.DCSTypes#Distance Distance The distance in meters. - function COORDINATE:Get3DDistance( TargetCoordinate ) - local TargetVec3 = TargetCoordinate:GetVec3() - local SourceVec3 = self:GetVec3() - return ( ( TargetVec3.x - SourceVec3.x ) ^ 2 + ( TargetVec3.y - SourceVec3.y ) ^ 2 + ( TargetVec3.z - SourceVec3.z ) ^ 2 ) ^ 0.5 - end - - - --- Provides a bearing text in degrees. - -- @param #COORDINATE self - -- @param #number AngleRadians The angle in randians. - -- @param #number Precision The precision. - -- @param Core.Settings#SETTINGS Settings - -- @return #string The bearing text in degrees. - function COORDINATE:GetBearingText( AngleRadians, Precision, Settings ) - - local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS - - local AngleDegrees = UTILS.Round( UTILS.ToDegree( AngleRadians ), Precision ) - - local s = string.format( '%03d°', AngleDegrees ) - - return s - end - - --- Provides a distance text expressed in the units of measurement. - -- @param #COORDINATE self - -- @param #number Distance The distance in meters. - -- @param Core.Settings#SETTINGS Settings - -- @return #string The distance text expressed in the units of measurement. - function COORDINATE:GetDistanceText( Distance, Settings ) - - local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS - - local DistanceText - - if Settings:IsMetric() then - DistanceText = " for " .. UTILS.Round( Distance / 1000, 2 ) .. " km" - else - DistanceText = " for " .. UTILS.Round( UTILS.MetersToNM( Distance ), 2 ) .. " miles" - end - - return DistanceText - end - - --- Return the altitude text of the COORDINATE. - -- @param #COORDINATE self - -- @return #string Altitude text. - function COORDINATE:GetAltitudeText( Settings ) - local Altitude = self.y - local Settings = Settings or _SETTINGS - if Altitude ~= 0 then - if Settings:IsMetric() then - return " at " .. UTILS.Round( self.y, -3 ) .. " meters" - else - return " at " .. UTILS.Round( UTILS.MetersToFeet( self.y ), -3 ) .. " feet" - end - else - return "" - end - end - - - - --- Return the velocity text of the COORDINATE. - -- @param #COORDINATE self - -- @return #string Velocity text. - function COORDINATE:GetVelocityText( Settings ) - local Velocity = self:GetVelocity() - local Settings = Settings or _SETTINGS - if Velocity then - if Settings:IsMetric() then - return string.format( " moving at %d km/h", UTILS.MpsToKmph( Velocity ) ) - else - return string.format( " moving at %d mi/h", UTILS.MpsToKmph( Velocity ) / 1.852 ) - end - else - return " stationary" - end - end - - - --- Return the heading text of the COORDINATE. - -- @param #COORDINATE self - -- @return #string Heading text. - function COORDINATE:GetHeadingText( Settings ) - local Heading = self:GetHeading() - if Heading then - return string.format( " bearing %3d°", Heading ) - else - return " bearing unknown" - end - end - - - --- Provides a Bearing / Range string - -- @param #COORDINATE self - -- @param #number AngleRadians The angle in randians - -- @param #number Distance The distance - -- @param Core.Settings#SETTINGS Settings - -- @return #string The BR Text - function COORDINATE:GetBRText( AngleRadians, Distance, Settings ) - - local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS - - local BearingText = self:GetBearingText( AngleRadians, 0, Settings ) - local DistanceText = self:GetDistanceText( Distance, Settings ) - - local BRText = BearingText .. DistanceText - - return BRText - end - - --- Provides a Bearing / Range / Altitude string - -- @param #COORDINATE self - -- @param #number AngleRadians The angle in randians - -- @param #number Distance The distance - -- @param Core.Settings#SETTINGS Settings - -- @return #string The BRA Text - function COORDINATE:GetBRAText( AngleRadians, Distance, Settings ) - - local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS - - local BearingText = self:GetBearingText( AngleRadians, 0, Settings ) - local DistanceText = self:GetDistanceText( Distance, Settings ) - local AltitudeText = self:GetAltitudeText( Settings ) - - local BRAText = BearingText .. DistanceText .. AltitudeText -- When the POINT is a VEC2, there will be no altitude shown. - - return BRAText - end - - - - --- Add a Distance in meters from the COORDINATE horizontal plane, with the given angle, and calculate the new COORDINATE. - -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Distance Distance The Distance to be added in meters. - -- @param Dcs.DCSTypes#Angle Angle The Angle in degrees. - -- @return #COORDINATE The new calculated COORDINATE. - function COORDINATE:Translate( Distance, Angle ) - local SX = self.x - local SZ = self.z - local Radians = Angle / 180 * math.pi - local TX = Distance * math.cos( Radians ) + SX - local TZ = Distance * math.sin( Radians ) + SZ - - return COORDINATE:New( TX, self.y, TZ ) - end - - - - --- Build an air type route point. - -- @param #COORDINATE self - -- @param #COORDINATE.RoutePointAltType AltType The altitude type. - -- @param #COORDINATE.RoutePointType Type The route point type. - -- @param #COORDINATE.RoutePointAction Action The route point action. - -- @param Dcs.DCSTypes#Speed Speed Airspeed in km/h. - -- @param #boolean SpeedLocked true means the speed is locked. - -- @return #table The route point. - function COORDINATE:WaypointAir( AltType, Type, Action, Speed, SpeedLocked ) - self:F2( { AltType, Type, Action, Speed, SpeedLocked } ) - - local RoutePoint = {} - RoutePoint.x = self.x - RoutePoint.y = self.z - RoutePoint.alt = self.y - RoutePoint.alt_type = AltType or "RADIO" - - RoutePoint.type = Type or nil - RoutePoint.action = Action or nil - - RoutePoint.speed = ( Speed and Speed / 3.6 ) or ( 500 / 3.6 ) - RoutePoint.speed_locked = true - - -- ["task"] = - -- { - -- ["id"] = "ComboTask", - -- ["params"] = - -- { - -- ["tasks"] = - -- { - -- }, -- end of ["tasks"] - -- }, -- end of ["params"] - -- }, -- end of ["task"] - - - RoutePoint.task = {} - RoutePoint.task.id = "ComboTask" - RoutePoint.task.params = {} - RoutePoint.task.params.tasks = {} - - - return RoutePoint - end - - --- Build an ground type route point. - -- @param #COORDINATE self - -- @param #number Speed (optional) Speed in km/h. The default speed is 999 km/h. - -- @param #string Formation (optional) The route point Formation, which is a text string that specifies exactly the Text in the Type of the route point, like "Vee", "Echelon Right". - -- @return #table The route point. - function COORDINATE:WaypointGround( Speed, Formation ) - self:F2( { Formation, Speed } ) - - local RoutePoint = {} - RoutePoint.x = self.x - RoutePoint.y = self.z - - RoutePoint.action = Formation or "" - - - RoutePoint.speed = ( Speed or 999 ) / 3.6 - RoutePoint.speed_locked = true - - -- ["task"] = - -- { - -- ["id"] = "ComboTask", - -- ["params"] = - -- { - -- ["tasks"] = - -- { - -- }, -- end of ["tasks"] - -- }, -- end of ["params"] - -- }, -- end of ["task"] - - - RoutePoint.task = {} - RoutePoint.task.id = "ComboTask" - RoutePoint.task.params = {} - RoutePoint.task.params.tasks = {} - - - return RoutePoint - end - - --- Creates an explosion at the point of a certain intensity. - -- @param #COORDINATE self - -- @param #number ExplosionIntensity - function COORDINATE:Explosion( ExplosionIntensity ) - self:F2( { ExplosionIntensity } ) - trigger.action.explosion( self:GetVec3(), ExplosionIntensity ) - end - - --- Creates an illumination bomb at the point. - -- @param #COORDINATE self - function COORDINATE:IlluminationBomb() - self:F2() - trigger.action.illuminationBomb( self:GetVec3() ) - end - - - --- Smokes the point in a color. - -- @param #COORDINATE self - -- @param Utilities.Utils#SMOKECOLOR SmokeColor - function COORDINATE:Smoke( SmokeColor ) - self:F2( { SmokeColor } ) - trigger.action.smoke( self:GetVec3(), SmokeColor ) - end - - --- Smoke the COORDINATE Green. - -- @param #COORDINATE self - function COORDINATE:SmokeGreen() - self:F2() - self:Smoke( SMOKECOLOR.Green ) - end - - --- Smoke the COORDINATE Red. - -- @param #COORDINATE self - function COORDINATE:SmokeRed() - self:F2() - self:Smoke( SMOKECOLOR.Red ) - end - - --- Smoke the COORDINATE White. - -- @param #COORDINATE self - function COORDINATE:SmokeWhite() - self:F2() - self:Smoke( SMOKECOLOR.White ) - end - - --- Smoke the COORDINATE Orange. - -- @param #COORDINATE self - function COORDINATE:SmokeOrange() - self:F2() - self:Smoke( SMOKECOLOR.Orange ) - end - - --- Smoke the COORDINATE Blue. - -- @param #COORDINATE self - function COORDINATE:SmokeBlue() - self:F2() - self:Smoke( SMOKECOLOR.Blue ) - end - - --- Flares the point in a color. - -- @param #COORDINATE self - -- @param Utilities.Utils#FLARECOLOR FlareColor - -- @param Dcs.DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. - function COORDINATE:Flare( FlareColor, Azimuth ) - self:F2( { FlareColor } ) - trigger.action.signalFlare( self:GetVec3(), FlareColor, Azimuth and Azimuth or 0 ) - end - - --- Flare the COORDINATE White. - -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. - function COORDINATE:FlareWhite( Azimuth ) - self:F2( Azimuth ) - self:Flare( FLARECOLOR.White, Azimuth ) - end - - --- Flare the COORDINATE Yellow. - -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. - function COORDINATE:FlareYellow( Azimuth ) - self:F2( Azimuth ) - self:Flare( FLARECOLOR.Yellow, Azimuth ) - end - - --- Flare the COORDINATE Green. - -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. - function COORDINATE:FlareGreen( Azimuth ) - self:F2( Azimuth ) - self:Flare( FLARECOLOR.Green, Azimuth ) - end - - --- Flare the COORDINATE Red. - -- @param #COORDINATE self - function COORDINATE:FlareRed( Azimuth ) - self:F2( Azimuth ) - self:Flare( FLARECOLOR.Red, Azimuth ) - end - - do -- Markings - - --- Mark to All - -- @param #COORDINATE self - -- @param #string MarkText Free format text that shows the marking clarification. - -- @return #number The resulting Mark ID which is a number. - -- @usage - -- local TargetCoord = TargetGroup:GetCoordinate() - -- local MarkID = TargetCoord:MarkToAll( "This is a target for all players" ) - function COORDINATE:MarkToAll( MarkText ) - local MarkID = UTILS.GetMarkID() - trigger.action.markToAll( MarkID, MarkText, self:GetVec3() ) - return MarkID - end - - --- Mark to Coalition - -- @param #COORDINATE self - -- @param #string MarkText Free format text that shows the marking clarification. - -- @param Coalition - -- @return #number The resulting Mark ID which is a number. - -- @usage - -- local TargetCoord = TargetGroup:GetCoordinate() - -- local MarkID = TargetCoord:MarkToCoalition( "This is a target for the red coalition", coalition.side.RED ) - function COORDINATE:MarkToCoalition( MarkText, Coalition ) - local MarkID = UTILS.GetMarkID() - trigger.action.markToCoalition( MarkID, MarkText, self:GetVec3(), Coalition ) - return MarkID - end - - --- Mark to Red Coalition - -- @param #COORDINATE self - -- @param #string MarkText Free format text that shows the marking clarification. - -- @return #number The resulting Mark ID which is a number. - -- @usage - -- local TargetCoord = TargetGroup:GetCoordinate() - -- local MarkID = TargetCoord:MarkToCoalitionRed( "This is a target for the red coalition" ) - function COORDINATE:MarkToCoalitionRed( MarkText ) - return self:MarkToCoalition( MarkText, coalition.side.RED ) - end - - --- Mark to Blue Coalition - -- @param #COORDINATE self - -- @param #string MarkText Free format text that shows the marking clarification. - -- @return #number The resulting Mark ID which is a number. - -- @usage - -- local TargetCoord = TargetGroup:GetCoordinate() - -- local MarkID = TargetCoord:MarkToCoalitionBlue( "This is a target for the blue coalition" ) - function COORDINATE:MarkToCoalitionBlue( MarkText ) - return self:MarkToCoalition( MarkText, coalition.side.BLUE ) - end - - --- Mark to Group - -- @param #COORDINATE self - -- @param #string MarkText Free format text that shows the marking clarification. - -- @param Wrapper.Group#GROUP MarkGroup The @{Group} that receives the mark. - -- @return #number The resulting Mark ID which is a number. - -- @usage - -- local TargetCoord = TargetGroup:GetCoordinate() - -- local MarkGroup = GROUP:FindByName( "AttackGroup" ) - -- local MarkID = TargetCoord:MarkToGroup( "This is a target for the attack group", AttackGroup ) - function COORDINATE:MarkToGroup( MarkText, MarkGroup ) - local MarkID = UTILS.GetMarkID() - trigger.action.markToGroup( MarkID, MarkText, self:GetVec3(), MarkGroup:GetID() ) - return MarkID - end - - --- Remove a mark - -- @param #COORDINATE self - -- @param #number MarkID The ID of the mark to be removed. - -- @usage - -- local TargetCoord = TargetGroup:GetCoordinate() - -- local MarkGroup = GROUP:FindByName( "AttackGroup" ) - -- local MarkID = TargetCoord:MarkToGroup( "This is a target for the attack group", AttackGroup ) - -- <<< logic >>> - -- RemoveMark( MarkID ) -- The mark is now removed - function COORDINATE:RemoveMark( MarkID ) - trigger.action.removeMark( MarkID ) - end - - end -- Markings - - - --- Returns if a Coordinate has Line of Sight (LOS) with the ToCoordinate. - -- @param #COORDINATE self - -- @param #COORDINATE ToCoordinate - -- @return #boolean true If the ToCoordinate has LOS with the Coordinate, otherwise false. - function COORDINATE:IsLOS( ToCoordinate ) - - -- Measurement of visibility should not be from the ground, so Adding a hypotethical 2 meters to each Coordinate. - local FromVec3 = self:GetVec3() - FromVec3.y = FromVec3.y + 2 - - local ToVec3 = ToCoordinate:GetVec3() - ToVec3.y = ToVec3.y + 2 - - local IsLOS = land.isVisible( FromVec3, ToVec3 ) - - return IsLOS - end - - - --- Returns if a Coordinate is in a certain Radius of this Coordinate in 2D plane using the X and Z axis. - -- @param #COORDINATE self - -- @param #COORDINATE ToCoordinate The coordinate that will be tested if it is in the radius of this coordinate. - -- @param #number Radius The radius of the circle on the 2D plane around this coordinate. - -- @return #boolean true if in the Radius. - function COORDINATE:IsInRadius( Coordinate, Radius ) - - local InVec2 = self:GetVec2() - local Vec2 = Coordinate:GetVec2() - - local InRadius = UTILS.IsInRadius( InVec2, Vec2, Radius) - - return InRadius - end - - - --- Returns if a Coordinate is in a certain radius of this Coordinate in 3D space using the X, Y and Z axis. - -- So Radius defines the radius of the a Sphere in 3D space around this coordinate. - -- @param #COORDINATE self - -- @param #COORDINATE ToCoordinate The coordinate that will be tested if it is in the radius of this coordinate. - -- @param #number Radius The radius of the sphere in the 3D space around this coordinate. - -- @return #boolean true if in the Sphere. - function COORDINATE:IsInSphere( Coordinate, Radius ) - - local InVec3 = self:GetVec3() - local Vec3 = Coordinate:GetVec3() - - local InSphere = UTILS.IsInSphere( InVec3, Vec3, Radius) - - return InSphere - end - - - --- Return a BR string from a COORDINATE to the COORDINATE. - -- @param #COORDINATE self - -- @param #COORDINATE TargetCoordinate The target COORDINATE. - -- @return #string The BR text. - function COORDINATE:ToStringBR( FromCoordinate, Settings ) - local DirectionVec3 = FromCoordinate:GetDirectionVec3( self ) - local AngleRadians = self:GetAngleRadians( DirectionVec3 ) - local Distance = self:Get2DDistance( FromCoordinate ) - return "BR, " .. self:GetBRText( AngleRadians, Distance, Settings ) - end - - --- Return a BRAA string from a COORDINATE to the COORDINATE. - -- @param #COORDINATE self - -- @param #COORDINATE TargetCoordinate The target COORDINATE. - -- @return #string The BR text. - function COORDINATE:ToStringBRA( FromCoordinate, Settings ) - local DirectionVec3 = FromCoordinate:GetDirectionVec3( self ) - local AngleRadians = self:GetAngleRadians( DirectionVec3 ) - local Distance = FromCoordinate:Get2DDistance( self ) - local Altitude = self:GetAltitudeText() - return "BRA, " .. self:GetBRAText( AngleRadians, Distance, Settings ) - end - - --- Return a BULLS string from a COORDINATE to the BULLS of the coalition. - -- @param #COORDINATE self - -- @param Dcs.DCSCoalition#coalition.side Coalition The coalition. - -- @return #string The BR text. - function COORDINATE:ToStringBULLS( Coalition, Settings ) - local TargetCoordinate = COORDINATE:NewFromVec3( coalition.getMainRefPoint( Coalition ) ) - local DirectionVec3 = self:GetDirectionVec3( TargetCoordinate ) - local AngleRadians = self:GetAngleRadians( DirectionVec3 ) - local Distance = self:Get2DDistance( TargetCoordinate ) - local Altitude = self:GetAltitudeText() - return "BULLS, " .. self:GetBRText( AngleRadians, Distance, Settings ) - end - - --- Return an aspect string from a COORDINATE to the Angle of the object. - -- @param #COORDINATE self - -- @param #COORDINATE TargetCoordinate The target COORDINATE. - -- @return #string The Aspect string, which is Hot, Cold or Flanking. - function COORDINATE:ToStringAspect( TargetCoordinate ) - local Heading = self.Heading - local DirectionVec3 = self:GetDirectionVec3( TargetCoordinate ) - local Angle = self:GetAngleDegrees( DirectionVec3 ) - - if Heading then - local Aspect = Angle - Heading - if Aspect > -135 and Aspect <= -45 then - return "Flanking" - end - if Aspect > -45 and Aspect <= 45 then - return "Hot" - end - if Aspect > 45 and Aspect <= 135 then - return "Flanking" - end - if Aspect > 135 or Aspect <= -135 then - return "Cold" - end - end - return "" - end - - --- Provides a Lat Lon string in Degree Minute Second format. - -- @param #COORDINATE self - -- @param Core.Settings#SETTINGS Settings (optional) Settings - -- @return #string The LL DMS Text - function COORDINATE:ToStringLLDMS( Settings ) - - local LL_Accuracy = Settings and Settings.LL_Accuracy or _SETTINGS.LL_Accuracy - local lat, lon = coord.LOtoLL( self:GetVec3() ) - return "LL DMS, " .. UTILS.tostringLL( lat, lon, LL_Accuracy, true ) - end - - --- Provides a Lat Lon string in Degree Decimal Minute format. - -- @param #COORDINATE self - -- @param Core.Settings#SETTINGS Settings (optional) Settings - -- @return #string The LL DDM Text - function COORDINATE:ToStringLLDDM( Settings ) - - local LL_Accuracy = Settings and Settings.LL_Accuracy or _SETTINGS.LL_Accuracy - local lat, lon = coord.LOtoLL( self:GetVec3() ) - return "LL DDM, " .. UTILS.tostringLL( lat, lon, LL_Accuracy, false ) - end - - --- Provides a MGRS string - -- @param #COORDINATE self - -- @param Core.Settings#SETTINGS Settings (optional) Settings - -- @return #string The MGRS Text - function COORDINATE:ToStringMGRS( Settings ) --R2.1 Fixes issue #424. - - local MGRS_Accuracy = Settings and Settings.MGRS_Accuracy or _SETTINGS.MGRS_Accuracy - local lat, lon = coord.LOtoLL( self:GetVec3() ) - local MGRS = coord.LLtoMGRS( lat, lon ) - return "MGRS, " .. UTILS.tostringMGRS( MGRS, MGRS_Accuracy ) - end - - --- Provides a coordinate string of the point, based on a coordinate format system: - -- * Uses default settings in COORDINATE. - -- * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default. - -- @param #COORDINATE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable - -- @param Core.Settings#SETTINGS Settings - -- @return #string The coordinate Text in the configured coordinate system. - function COORDINATE:ToStringFromRP( ReferenceCoord, ReferenceName, Controllable, Settings ) -- R2.2 - - self:E( { ReferenceCoord = ReferenceCoord, ReferenceName = ReferenceName } ) - - local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS - - local IsAir = Controllable and Controllable:IsAirPlane() or false - - if IsAir then - local DirectionVec3 = ReferenceCoord:GetDirectionVec3( self ) - local AngleRadians = self:GetAngleRadians( DirectionVec3 ) - local Distance = self:Get2DDistance( ReferenceCoord ) - return "Targets are the last seen " .. self:GetBRText( AngleRadians, Distance, Settings ) .. " from " .. ReferenceName - else - local DirectionVec3 = ReferenceCoord:GetDirectionVec3( self ) - local AngleRadians = self:GetAngleRadians( DirectionVec3 ) - local Distance = self:Get2DDistance( ReferenceCoord ) - return "Target are located " .. self:GetBRText( AngleRadians, Distance, Settings ) .. " from " .. ReferenceName - end - - return nil - - end - - --- Provides a coordinate string of the point, based on the A2G coordinate format system. - -- @param #COORDINATE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable - -- @param Core.Settings#SETTINGS Settings - -- @return #string The coordinate Text in the configured coordinate system. - function COORDINATE:ToStringA2G( Controllable, Settings ) -- R2.2 - - self:F( { Controllable = Controllable and Controllable:GetName() } ) - - local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS - - if Settings:IsA2G_BR() then - -- If no Controllable is given to calculate the BR from, then MGRS will be used!!! - if Controllable then - local Coordinate = Controllable:GetCoordinate() - return Controllable and self:ToStringBR( Coordinate, Settings ) or self:ToStringMGRS( Settings ) - else - return self:ToStringMGRS( Settings ) - end - end - if Settings:IsA2G_LL_DMS() then - return self:ToStringLLDMS( Settings ) - end - if Settings:IsA2G_LL_DDM() then - return self:ToStringLLDDM( Settings ) - end - if Settings:IsA2G_MGRS() then - return self:ToStringMGRS( Settings ) - end - - return nil - - end - - - --- Provides a coordinate string of the point, based on the A2A coordinate format system. - -- @param #COORDINATE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable - -- @param Core.Settings#SETTINGS Settings - -- @return #string The coordinate Text in the configured coordinate system. - function COORDINATE:ToStringA2A( Controllable, Settings ) -- R2.2 - - self:F( { Controllable = Controllable and Controllable:GetName() } ) - - local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS - - if Settings:IsA2A_BRAA() then - if Controllable then - local Coordinate = Controllable:GetCoordinate() - return self:ToStringBRA( Coordinate, Settings ) - else - return self:ToStringMGRS( Settings ) - end - end - if Settings:IsA2A_BULLS() then - local Coalition = Controllable:GetCoalition() - return self:ToStringBULLS( Coalition, Settings ) - end - if Settings:IsA2A_LL_DMS() then - return self:ToStringLLDMS( Settings ) - end - if Settings:IsA2A_LL_DDM() then - return self:ToStringLLDDM( Settings ) - end - if Settings:IsA2A_MGRS() then - return self:ToStringMGRS( Settings ) - end - - return nil - - end - - --- Provides a coordinate string of the point, based on a coordinate format system: - -- * Uses default settings in COORDINATE. - -- * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default. - -- @param #COORDINATE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable - -- @param Core.Settings#SETTINGS Settings - -- @param Tasking.Task#TASK Task The task for which coordinates need to be calculated. - -- @return #string The coordinate Text in the configured coordinate system. - function COORDINATE:ToString( Controllable, Settings, Task ) -- R2.2 - - self:F( { Controllable = Controllable and Controllable:GetName() } ) - - local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS - - local ModeA2A = true - - if Task then - if Task:IsInstanceOf( TASK_A2A ) then - ModeA2A = true - else - if Task:IsInstanceOf( TASK_A2G ) then - ModeA2A = false - else - if Task:IsInstanceOf( TASK_CARGO ) then - ModeA2A = false - end - end - end - else - local IsAir = Controllable and Controllable:IsAirPlane() or false - if IsAir then - ModeA2A = true - else - ModeA2A = false - end - end - - - if ModeA2A == true then - return self:ToStringA2A( Controllable, Settings ) - else - return self:ToStringA2G( Controllable, Settings ) - end - - return nil - - end - -end - -do -- POINT_VEC3 - - --- The POINT_VEC3 class - -- @type POINT_VEC3 - -- @field #number x The x coordinate in 3D space. - -- @field #number y The y coordinate in 3D space. - -- @field #number z The z coordiante in 3D space. - -- @field Utilities.Utils#SMOKECOLOR SmokeColor - -- @field Utilities.Utils#FLARECOLOR FlareColor - -- @field #POINT_VEC3.RoutePointAltType RoutePointAltType - -- @field #POINT_VEC3.RoutePointType RoutePointType - -- @field #POINT_VEC3.RoutePointAction RoutePointAction - -- @extends Core.Point#COORDINATE - - - --- # POINT_VEC3 class, extends @{Point#COORDINATE} - -- - -- POINT_VEC3 defines a 3D point in the simulator and with its methods, you can use or manipulate the point in 3D space. - -- - -- **Important Note:** Most of the functions in this section were taken from MIST, and reworked to OO concepts. - -- In order to keep the credibility of the the author, - -- I want to emphasize that the formulas embedded in the MIST framework were created by Grimes or previous authors, - -- who you can find on the Eagle Dynamics Forums. - -- - -- - -- ## POINT_VEC3 constructor - -- - -- A new POINT_VEC3 object can be created with: - -- - -- * @{#POINT_VEC3.New}(): a 3D point. - -- * @{#POINT_VEC3.NewFromVec3}(): a 3D point created from a @{DCSTypes#Vec3}. - -- - -- - -- ## Manupulate the X, Y, Z coordinates of the POINT_VEC3 - -- - -- A POINT_VEC3 class works in 3D space. It contains internally an X, Y, Z coordinate. - -- Methods exist to manupulate these coordinates. - -- - -- The current X, Y, Z axis can be retrieved with the methods @{#POINT_VEC3.GetX}(), @{#POINT_VEC3.GetY}(), @{#POINT_VEC3.GetZ}() respectively. - -- The methods @{#POINT_VEC3.SetX}(), @{#POINT_VEC3.SetY}(), @{#POINT_VEC3.SetZ}() change the respective axis with a new value. - -- The current axis values can be changed by using the methods @{#POINT_VEC3.AddX}(), @{#POINT_VEC3.AddY}(), @{#POINT_VEC3.AddZ}() - -- to add or substract a value from the current respective axis value. - -- Note that the Set and Add methods return the current POINT_VEC3 object, so these manipulation methods can be chained... For example: - -- - -- local Vec3 = PointVec3:AddX( 100 ):AddZ( 150 ):GetVec3() - -- - -- - -- ## 3D calculation methods - -- - -- Various calculation methods exist to use or manipulate 3D space. Find below a short description of each method: - -- - -- - -- ## Point Randomization - -- - -- Various methods exist to calculate random locations around a given 3D point. - -- - -- * @{#POINT_VEC3.GetRandomPointVec3InRadius}(): Provides a random 3D point around the current 3D point, in the given inner to outer band. - -- - -- - -- @field #POINT_VEC3 - POINT_VEC3 = { - ClassName = "POINT_VEC3", - Metric = true, - RoutePointAltType = { - BARO = "BARO", - }, - RoutePointType = { - TakeOffParking = "TakeOffParking", - TurningPoint = "Turning Point", - }, - RoutePointAction = { - FromParkingArea = "From Parking Area", - TurningPoint = "Turning Point", - }, - } - - --- RoutePoint AltTypes - -- @type POINT_VEC3.RoutePointAltType - -- @field BARO "BARO" - - --- RoutePoint Types - -- @type POINT_VEC3.RoutePointType - -- @field TakeOffParking "TakeOffParking" - -- @field TurningPoint "Turning Point" - - --- RoutePoint Actions - -- @type POINT_VEC3.RoutePointAction - -- @field FromParkingArea "From Parking Area" - -- @field TurningPoint "Turning Point" - - -- Constructor. - - --- Create a new POINT_VEC3 object. - -- @param #POINT_VEC3 self - -- @param Dcs.DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. - -- @param Dcs.DCSTypes#Distance y The y coordinate of the Vec3 point, pointing Upwards. - -- @param Dcs.DCSTypes#Distance z The z coordinate of the Vec3 point, pointing to the Right. - -- @return Core.Point#POINT_VEC3 - function POINT_VEC3:New( x, y, z ) - - local self = BASE:Inherit( self, COORDINATE:New( x, y, z ) ) -- Core.Point#POINT_VEC3 - self:F2( self ) - - return self - end - - --- Create a new POINT_VEC3 object from Vec2 coordinates. - -- @param #POINT_VEC3 self - -- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 point. - -- @param Dcs.DCSTypes#Distance LandHeightAdd (optional) Add a landheight. - -- @return Core.Point#POINT_VEC3 self - function POINT_VEC3:NewFromVec2( Vec2, LandHeightAdd ) - - local self = BASE:Inherit( self, COORDINATE:NewFromVec2( Vec2, LandHeightAdd ) ) -- Core.Point#POINT_VEC3 - self:F2( self ) - - return self - end - - - --- Create a new POINT_VEC3 object from Vec3 coordinates. - -- @param #POINT_VEC3 self - -- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 point. - -- @return Core.Point#POINT_VEC3 self - function POINT_VEC3:NewFromVec3( Vec3 ) - - local self = BASE:Inherit( self, COORDINATE:NewFromVec3( Vec3 ) ) -- Core.Point#POINT_VEC3 - self:F2( self ) - - return self - end - - - - --- Return the x coordinate of the POINT_VEC3. - -- @param #POINT_VEC3 self - -- @return #number The x coodinate. - function POINT_VEC3:GetX() - return self.x - end - - --- Return the y coordinate of the POINT_VEC3. - -- @param #POINT_VEC3 self - -- @return #number The y coodinate. - function POINT_VEC3:GetY() - return self.y - end - - --- Return the z coordinate of the POINT_VEC3. - -- @param #POINT_VEC3 self - -- @return #number The z coodinate. - function POINT_VEC3:GetZ() - return self.z - end - - --- Set the x coordinate of the POINT_VEC3. - -- @param #POINT_VEC3 self - -- @param #number x The x coordinate. - -- @return #POINT_VEC3 - function POINT_VEC3:SetX( x ) - self.x = x - return self - end - - --- Set the y coordinate of the POINT_VEC3. - -- @param #POINT_VEC3 self - -- @param #number y The y coordinate. - -- @return #POINT_VEC3 - function POINT_VEC3:SetY( y ) - self.y = y - return self - end - - --- Set the z coordinate of the POINT_VEC3. - -- @param #POINT_VEC3 self - -- @param #number z The z coordinate. - -- @return #POINT_VEC3 - function POINT_VEC3:SetZ( z ) - self.z = z - return self - end - - --- Add to the x coordinate of the POINT_VEC3. - -- @param #POINT_VEC3 self - -- @param #number x The x coordinate value to add to the current x coodinate. - -- @return #POINT_VEC3 - function POINT_VEC3:AddX( x ) - self.x = self.x + x - return self - end - - --- Add to the y coordinate of the POINT_VEC3. - -- @param #POINT_VEC3 self - -- @param #number y The y coordinate value to add to the current y coodinate. - -- @return #POINT_VEC3 - function POINT_VEC3:AddY( y ) - self.y = self.y + y - return self - end - - --- Add to the z coordinate of the POINT_VEC3. - -- @param #POINT_VEC3 self - -- @param #number z The z coordinate value to add to the current z coodinate. - -- @return #POINT_VEC3 - function POINT_VEC3:AddZ( z ) - self.z = self.z +z - return self - end - - --- Return a random POINT_VEC3 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. - -- @param #POINT_VEC3 self - -- @param Dcs.DCSTypes#Distance OuterRadius - -- @param Dcs.DCSTypes#Distance InnerRadius - -- @return #POINT_VEC3 - function POINT_VEC3:GetRandomPointVec3InRadius( OuterRadius, InnerRadius ) - - return POINT_VEC3:NewFromVec3( self:GetRandomVec3InRadius( OuterRadius, InnerRadius ) ) - end - -end - -do -- POINT_VEC2 - - --- @type POINT_VEC2 - -- @field Dcs.DCSTypes#Distance x The x coordinate in meters. - -- @field Dcs.DCSTypes#Distance y the y coordinate in meters. - -- @extends Core.Point#COORDINATE - - --- # POINT_VEC2 class, extends @{Point#COORDINATE} - -- - -- The @{Point#POINT_VEC2} class defines a 2D point in the simulator. The height coordinate (if needed) will be the land height + an optional added height specified. - -- - -- ## POINT_VEC2 constructor - -- - -- A new POINT_VEC2 instance can be created with: - -- - -- * @{Point#POINT_VEC2.New}(): a 2D point, taking an additional height parameter. - -- * @{Point#POINT_VEC2.NewFromVec2}(): a 2D point created from a @{DCSTypes#Vec2}. - -- - -- ## Manupulate the X, Altitude, Y coordinates of the 2D point - -- - -- A POINT_VEC2 class works in 2D space, with an altitude setting. It contains internally an X, Altitude, Y coordinate. - -- Methods exist to manupulate these coordinates. - -- - -- The current X, Altitude, Y axis can be retrieved with the methods @{#POINT_VEC2.GetX}(), @{#POINT_VEC2.GetAlt}(), @{#POINT_VEC2.GetY}() respectively. - -- The methods @{#POINT_VEC2.SetX}(), @{#POINT_VEC2.SetAlt}(), @{#POINT_VEC2.SetY}() change the respective axis with a new value. - -- The current Lat(itude), Alt(itude), Lon(gitude) values can also be retrieved with the methods @{#POINT_VEC2.GetLat}(), @{#POINT_VEC2.GetAlt}(), @{#POINT_VEC2.GetLon}() respectively. - -- The current axis values can be changed by using the methods @{#POINT_VEC2.AddX}(), @{#POINT_VEC2.AddAlt}(), @{#POINT_VEC2.AddY}() - -- to add or substract a value from the current respective axis value. - -- Note that the Set and Add methods return the current POINT_VEC2 object, so these manipulation methods can be chained... For example: - -- - -- local Vec2 = PointVec2:AddX( 100 ):AddY( 2000 ):GetVec2() - -- - -- @field #POINT_VEC2 - POINT_VEC2 = { - ClassName = "POINT_VEC2", - } - - - - --- POINT_VEC2 constructor. - -- @param #POINT_VEC2 self - -- @param Dcs.DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. - -- @param Dcs.DCSTypes#Distance y The y coordinate of the Vec3 point, pointing to the Right. - -- @param Dcs.DCSTypes#Distance LandHeightAdd (optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. - -- @return Core.Point#POINT_VEC2 - function POINT_VEC2:New( x, y, LandHeightAdd ) - - local LandHeight = land.getHeight( { ["x"] = x, ["y"] = y } ) - - LandHeightAdd = LandHeightAdd or 0 - LandHeight = LandHeight + LandHeightAdd - - local self = BASE:Inherit( self, COORDINATE:New( x, LandHeight, y ) ) -- Core.Point#POINT_VEC2 - self:F2( self ) - - return self - end - - --- Create a new POINT_VEC2 object from Vec2 coordinates. - -- @param #POINT_VEC2 self - -- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 point. - -- @return Core.Point#POINT_VEC2 self - function POINT_VEC2:NewFromVec2( Vec2, LandHeightAdd ) - - local LandHeight = land.getHeight( Vec2 ) - - LandHeightAdd = LandHeightAdd or 0 - LandHeight = LandHeight + LandHeightAdd - - local self = BASE:Inherit( self, COORDINATE:NewFromVec2( Vec2, LandHeightAdd ) ) -- #POINT_VEC2 - self:F2( self ) - - return self - end - - --- Create a new POINT_VEC2 object from Vec3 coordinates. - -- @param #POINT_VEC2 self - -- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 point. - -- @return Core.Point#POINT_VEC2 self - function POINT_VEC2:NewFromVec3( Vec3 ) - - local self = BASE:Inherit( self, COORDINATE:NewFromVec3( Vec3 ) ) -- #POINT_VEC2 - self:F2( self ) - - return self - end - - --- Return the x coordinate of the POINT_VEC2. - -- @param #POINT_VEC2 self - -- @return #number The x coodinate. - function POINT_VEC2:GetX() - return self.x - end - - --- Return the y coordinate of the POINT_VEC2. - -- @param #POINT_VEC2 self - -- @return #number The y coodinate. - function POINT_VEC2:GetY() - return self.z - end - - --- Set the x coordinate of the POINT_VEC2. - -- @param #POINT_VEC2 self - -- @param #number x The x coordinate. - -- @return #POINT_VEC2 - function POINT_VEC2:SetX( x ) - self.x = x - return self - end - - --- Set the y coordinate of the POINT_VEC2. - -- @param #POINT_VEC2 self - -- @param #number y The y coordinate. - -- @return #POINT_VEC2 - function POINT_VEC2:SetY( y ) - self.z = y - return self - end - - --- Return Return the Lat(itude) coordinate of the POINT_VEC2 (ie: (parent)POINT_VEC3.x). - -- @param #POINT_VEC2 self - -- @return #number The x coodinate. - function POINT_VEC2:GetLat() - return self.x - end - - --- Set the Lat(itude) coordinate of the POINT_VEC2 (ie: POINT_VEC3.x). - -- @param #POINT_VEC2 self - -- @param #number x The x coordinate. - -- @return #POINT_VEC2 - function POINT_VEC2:SetLat( x ) - self.x = x - return self - end - - --- Return the Lon(gitude) coordinate of the POINT_VEC2 (ie: (parent)POINT_VEC3.z). - -- @param #POINT_VEC2 self - -- @return #number The y coodinate. - function POINT_VEC2:GetLon() - return self.z - end - - --- Set the Lon(gitude) coordinate of the POINT_VEC2 (ie: POINT_VEC3.z). - -- @param #POINT_VEC2 self - -- @param #number y The y coordinate. - -- @return #POINT_VEC2 - function POINT_VEC2:SetLon( z ) - self.z = z - return self - end - - --- Return the altitude (height) of the land at the POINT_VEC2. - -- @param #POINT_VEC2 self - -- @return #number The land altitude. - function POINT_VEC2:GetAlt() - return self.y ~= 0 or land.getHeight( { x = self.x, y = self.z } ) - end - - --- Set the altitude of the POINT_VEC2. - -- @param #POINT_VEC2 self - -- @param #number Altitude The land altitude. If nothing (nil) is given, then the current land altitude is set. - -- @return #POINT_VEC2 - function POINT_VEC2:SetAlt( Altitude ) - self.y = Altitude or land.getHeight( { x = self.x, y = self.z } ) - return self - end - - --- Add to the x coordinate of the POINT_VEC2. - -- @param #POINT_VEC2 self - -- @param #number x The x coordinate. - -- @return #POINT_VEC2 - function POINT_VEC2:AddX( x ) - self.x = self.x + x - return self - end - - --- Add to the y coordinate of the POINT_VEC2. - -- @param #POINT_VEC2 self - -- @param #number y The y coordinate. - -- @return #POINT_VEC2 - function POINT_VEC2:AddY( y ) - self.z = self.z + y - return self - end - - --- Add to the current land height an altitude. - -- @param #POINT_VEC2 self - -- @param #number Altitude The Altitude to add. If nothing (nil) is given, then the current land altitude is set. - -- @return #POINT_VEC2 - function POINT_VEC2:AddAlt( Altitude ) - self.y = land.getHeight( { x = self.x, y = self.z } ) + Altitude or 0 - return self - end - - - --- Return a random POINT_VEC2 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC2. - -- @param #POINT_VEC2 self - -- @param Dcs.DCSTypes#Distance OuterRadius - -- @param Dcs.DCSTypes#Distance InnerRadius - -- @return #POINT_VEC2 - function POINT_VEC2:GetRandomPointVec2InRadius( OuterRadius, InnerRadius ) - self:F2( { OuterRadius, InnerRadius } ) - - return POINT_VEC2:NewFromVec2( self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) ) - end - - -- TODO: Check this to replace - --- Calculate the distance from a reference @{#POINT_VEC2}. - -- @param #POINT_VEC2 self - -- @param #POINT_VEC2 PointVec2Reference The reference @{#POINT_VEC2}. - -- @return Dcs.DCSTypes#Distance The distance from the reference @{#POINT_VEC2} in meters. - function POINT_VEC2:DistanceFromPointVec2( PointVec2Reference ) - self:F2( PointVec2Reference ) - - local Distance = ( ( PointVec2Reference.x - self.x ) ^ 2 + ( PointVec2Reference.z - self.z ) ^2 ) ^ 0.5 - - self:T2( Distance ) - return Distance - end - -end - - ---- **Core** -- MESSAGE class takes are of the **real-time notifications** and **messages to players** during a simulation. --- --- ![Banner Image](..\Presentations\MESSAGE\Dia1.JPG) --- --- === --- --- @module Message - ---- The MESSAGE class --- @type MESSAGE --- @extends Core.Base#BASE - ---- # MESSAGE class, extends @{Base#BASE} --- --- Message System to display Messages to Clients, Coalitions or All. --- Messages are shown on the display panel for an amount of seconds, and will then disappear. --- Messages can contain a category which is indicating the category of the message. --- --- ## MESSAGE construction --- --- Messages are created with @{Message#MESSAGE.New}. Note that when the MESSAGE object is created, no message is sent yet. --- To send messages, you need to use the To functions. --- --- ## Send messages to an audience --- --- Messages are sent: --- --- * To a @{Client} using @{Message#MESSAGE.ToClient}(). --- * To a @{Group} using @{Message#MESSAGE.ToGroup}() --- * To a coalition using @{Message#MESSAGE.ToCoalition}(). --- * To the red coalition using @{Message#MESSAGE.ToRed}(). --- * To the blue coalition using @{Message#MESSAGE.ToBlue}(). --- * To all Players using @{Message#MESSAGE.ToAll}(). --- --- ## Send conditionally to an audience --- --- Messages can be sent conditionally to an audience (when a condition is true): --- --- * To all players using @{Message#MESSAGE.ToAllIf}(). --- * To a coalition using @{Message#MESSAGE.ToCoalitionIf}(). --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- ### Contributions: --- --- ==== --- --- @field #MESSAGE -MESSAGE = { - ClassName = "MESSAGE", - MessageCategory = 0, - MessageID = 0, -} - ---- Message Types --- @type MESSAGE.Type -MESSAGE.Type = { - Update = "Update", - Information = "Information", - Briefing = "Briefing Report", - Overview = "Overview Report", - Detailed = "Detailed Report" -} - - ---- Creates a new MESSAGE object. Note that these MESSAGE objects are not yet displayed on the display panel. You must use the functions @{ToClient} or @{ToCoalition} or @{ToAll} to send these Messages to the respective recipients. --- @param self --- @param #string MessageText is the text of the Message. --- @param #number MessageDuration is a number in seconds of how long the MESSAGE should be shown on the display panel. --- @param #string MessageCategory (optional) is a string expressing the "category" of the Message. The category will be shown as the first text in the message followed by a ": ". --- @return #MESSAGE --- @usage --- -- Create a series of new Messages. --- -- MessageAll is meant to be sent to all players, for 25 seconds, and is classified as "Score". --- -- MessageRED is meant to be sent to the RED players only, for 10 seconds, and is classified as "End of Mission", with ID "Win". --- -- MessageClient1 is meant to be sent to a Client, for 25 seconds, and is classified as "Score", with ID "Score". --- -- MessageClient1 is meant to be sent to a Client, for 25 seconds, and is classified as "Score", with ID "Score". --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", 25, "End of Mission" ) --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", 25, "Penalty" ) --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", 25, "Score" ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", 25, "Score") -function MESSAGE:New( MessageText, MessageDuration, MessageCategory ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { MessageText, MessageDuration, MessageCategory } ) - - - self.MessageType = nil - - -- When no MessageCategory is given, we don't show it as a title... - if MessageCategory and MessageCategory ~= "" then - if MessageCategory:sub(-1) ~= "\n" then - self.MessageCategory = MessageCategory .. ": " - else - self.MessageCategory = MessageCategory:sub( 1, -2 ) .. ":\n" - end - else - self.MessageCategory = "" - end - - self.MessageDuration = MessageDuration or 5 - self.MessageTime = timer.getTime() - self.MessageText = MessageText:gsub("^\n","",1):gsub("\n$","",1) - - self.MessageSent = false - self.MessageGroup = false - self.MessageCoalition = false - - return self -end - - ---- Creates a new MESSAGE object of a certain type. --- Note that these MESSAGE objects are not yet displayed on the display panel. --- You must use the functions @{ToClient} or @{ToCoalition} or @{ToAll} to send these Messages to the respective recipients. --- The message display times are automatically defined based on the timing settings in the @{Settings} menu. --- @param self --- @param #string MessageText is the text of the Message. --- @param #MESSAGE.Type MessageType The type of the message. --- @return #MESSAGE --- @usage --- MessageAll = MESSAGE:NewType( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", MESSAGE.Type.Information ) --- MessageRED = MESSAGE:NewType( "To the RED Players: You receive a penalty because you've killed one of your own units", MESSAGE.Type.Information ) --- MessageClient1 = MESSAGE:NewType( "Congratulations, you've just hit a target", MESSAGE.Type.Update ) --- MessageClient2 = MESSAGE:NewType( "Congratulations, you've just killed a target", MESSAGE.Type.Update ) -function MESSAGE:NewType( MessageText, MessageType ) - - local self = BASE:Inherit( self, BASE:New() ) - self:F( { MessageText } ) - - self.MessageType = MessageType - - self.MessageTime = timer.getTime() - self.MessageText = MessageText:gsub("^\n","",1):gsub("\n$","",1) - - return self -end - - - - - ---- Sends a MESSAGE to a Client Group. Note that the Group needs to be defined within the ME with the skillset "Client" or "Player". --- @param #MESSAGE self --- @param Wrapper.Client#CLIENT Client is the Group of the Client. --- @return #MESSAGE --- @usage --- -- Send the 2 messages created with the @{New} method to the Client Group. --- -- Note that the Message of MessageClient2 is overwriting the Message of MessageClient1. --- ClientGroup = Group.getByName( "ClientGroup" ) --- --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- or --- MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- or --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ) --- MessageClient1:ToClient( ClientGroup ) --- MessageClient2:ToClient( ClientGroup ) -function MESSAGE:ToClient( Client, Settings ) - self:F( Client ) - - if Client and Client:GetClientGroupID() then - - if self.MessageType then - local Settings = Settings or ( Client and _DATABASE:GetPlayerSettings( Client:GetPlayerName() ) ) or _SETTINGS -- Core.Settings#SETTINGS - self.MessageDuration = Settings:GetMessageTime( self.MessageType ) - self.MessageCategory = self.MessageType .. ": " - end - - if self.MessageDuration ~= 0 then - local ClientGroupID = Client:GetClientGroupID() - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForGroup( ClientGroupID, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) - end - end - - return self -end - ---- Sends a MESSAGE to a Group. --- @param #MESSAGE self --- @param Wrapper.Group#GROUP Group is the Group. --- @return #MESSAGE -function MESSAGE:ToGroup( Group, Settings ) - self:F( Group.GroupName ) - - if Group then - - if self.MessageType then - local Settings = Settings or ( Group and _DATABASE:GetPlayerSettings( Group:GetPlayerName() ) ) or _SETTINGS -- Core.Settings#SETTINGS - self.MessageDuration = Settings:GetMessageTime( self.MessageType ) - self.MessageCategory = self.MessageType .. ": " - end - - if self.MessageDuration ~= 0 then - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForGroup( Group:GetID(), self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) - end - end - - return self -end ---- Sends a MESSAGE to the Blue coalition. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the BLUE coalition. --- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() --- or --- MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() --- or --- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageBLUE:ToBlue() -function MESSAGE:ToBlue() - self:F() - - self:ToCoalition( coalition.side.BLUE ) - - return self -end - ---- Sends a MESSAGE to the Red Coalition. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the RED coalition. --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() --- or --- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() --- or --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageRED:ToRed() -function MESSAGE:ToRed( ) - self:F() - - self:ToCoalition( coalition.side.RED ) - - return self -end - ---- Sends a MESSAGE to a Coalition. --- @param #MESSAGE self --- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the RED coalition. --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) --- or --- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) --- or --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageRED:ToCoalition( coalition.side.RED ) -function MESSAGE:ToCoalition( CoalitionSide, Settings ) - self:F( CoalitionSide ) - - if self.MessageType then - local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS - self.MessageDuration = Settings:GetMessageTime( self.MessageType ) - self.MessageCategory = self.MessageType .. ": " - end - - if CoalitionSide then - if self.MessageDuration ~= 0 then - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForCoalition( CoalitionSide, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) - end - end - - return self -end - ---- Sends a MESSAGE to a Coalition if the given Condition is true. --- @param #MESSAGE self --- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. --- @return #MESSAGE -function MESSAGE:ToCoalitionIf( CoalitionSide, Condition ) - self:F( CoalitionSide ) - - if Condition and Condition == true then - self:ToCoalition( CoalitionSide ) - end - - return self -end - ---- Sends a MESSAGE to all players. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created to all players. --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() --- or --- MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() --- or --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ) --- MessageAll:ToAll() -function MESSAGE:ToAll() - self:F() - - if self.MessageType then - local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS - self.MessageDuration = Settings:GetMessageTime( self.MessageType ) - self.MessageCategory = self.MessageType .. ": " - end - - if self.MessageDuration ~= 0 then - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outText( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) - end - - return self -end - - ---- Sends a MESSAGE to all players if the given Condition is true. --- @param #MESSAGE self --- @return #MESSAGE -function MESSAGE:ToAllIf( Condition ) - - if Condition and Condition == true then - self:ToAll() - end - - return self -end ---- **Core** -- The **FSM** (**F**inite **S**tate **M**achine) class and derived **FSM\_** classes --- are design patterns allowing efficient (long-lasting) processes and workflows. --- --- ![Banner Image](..\Presentations\FSM\Dia1.JPG) --- --- === --- --- A Finite State Machine (FSM) models a process flow that transitions between various **States** through triggered **Events**. --- --- A FSM can only be in one of a finite number of states. --- The machine is in only one state at a time; the state it is in at any given time is called the **current state**. --- It can change from one state to another when initiated by an **__internal__ or __external__ triggering event**, which is called a **transition**. --- An **FSM implementation** is defined by **a list of its states**, **its initial state**, and **the triggering events** for **each possible transition**. --- An FSM implementation is composed out of **two parts**, a set of **state transition rules**, and an implementation set of **state transition handlers**, implementing those transitions. --- --- The FSM class supports a **hierarchical implementation of a Finite State Machine**, --- that is, it allows to **embed existing FSM implementations in a master FSM**. --- FSM hierarchies allow for efficient FSM re-use, **not having to re-invent the wheel every time again** when designing complex processes. --- --- ![Workflow Example](..\Presentations\FSM\Dia2.JPG) --- --- The above diagram shows a graphical representation of a FSM implementation for a **Task**, which guides a Human towards a Zone, --- orders him to destroy x targets and account the results. --- Other examples of ready made FSM could be: --- --- * route a plane to a zone flown by a human --- * detect targets by an AI and report to humans --- * account for destroyed targets by human players --- * handle AI infantry to deploy from or embark to a helicopter or airplane or vehicle --- * let an AI patrol a zone --- --- The **MOOSE framework** uses extensively the FSM class and derived FSM\_ classes, --- because **the goal of MOOSE is to simplify mission design complexity for mission building**. --- By efficiently utilizing the FSM class and derived classes, MOOSE allows mission designers to quickly build processes. --- **Ready made FSM-based implementations classes** exist within the MOOSE framework that **can easily be re-used, --- and tailored** by mission designers through **the implementation of Transition Handlers**. --- Each of these FSM implementation classes start either with: --- --- * an acronym **AI\_**, which indicates an FSM implementation directing **AI controlled** @{GROUP} and/or @{UNIT}. These AI\_ classes derive the @{#FSM_CONTROLLABLE} class. --- * an acronym **TASK\_**, which indicates an FSM implementation executing a @{TASK} executed by Groups of players. These TASK\_ classes derive the @{#FSM_TASK} class. --- * an acronym **ACT\_**, which indicates an Sub-FSM implementation, directing **Humans actions** that need to be done in a @{TASK}, seated in a @{CLIENT} (slot) or a @{UNIT} (CA join). These ACT\_ classes derive the @{#FSM_PROCESS} class. --- --- Detailed explanations and API specifics are further below clarified and FSM derived class specifics are described in those class documentation sections. --- --- ##__Dislaimer:__ --- The FSM class development is based on a finite state machine implementation made by Conroy Kyle. --- The state machine can be found on [github](https://github.com/kyleconroy/lua-state-machine) --- I've reworked this development (taken the concept), and created a **hierarchical state machine** out of it, embedded within the DCS simulator. --- Additionally, I've added extendability and created an API that allows seamless FSM implementation. --- --- The following derived classes are available in the MOOSE framework, that implement a specialised form of a FSM: --- --- * @{#FSM_TASK}: Models Finite State Machines for @{Task}s. --- * @{#FSM_PROCESS}: Models Finite State Machines for @{Task} actions, which control @{Client}s. --- * @{#FSM_CONTROLLABLE}: Models Finite State Machines for @{Controllable}s, which are @{Group}s, @{Unit}s, @{Client}s. --- * @{#FSM_SET}: Models Finite State Machines for @{Set}s. Note that these FSMs control multiple objects!!! So State concerns here --- for multiple objects or the position of the state machine in the process. --- --- ==== --- --- --- ### Author: **Sven Van de Velde (FlightControl)** --- ### Contributions: --- --- ==== --- --- @module Fsm - -do -- FSM - - --- @type FSM - -- @extends Core.Base#BASE - - - --- # FSM class, extends @{Base#BASE} - -- - -- A Finite State Machine (FSM) models a process flow that transitions between various **States** through triggered **Events**. - -- - -- A FSM can only be in one of a finite number of states. - -- The machine is in only one state at a time; the state it is in at any given time is called the **current state**. - -- It can change from one state to another when initiated by an **__internal__ or __external__ triggering event**, which is called a **transition**. - -- An **FSM implementation** is defined by **a list of its states**, **its initial state**, and **the triggering events** for **each possible transition**. - -- An FSM implementation is composed out of **two parts**, a set of **state transition rules**, and an implementation set of **state transition handlers**, implementing those transitions. - -- - -- The FSM class supports a **hierarchical implementation of a Finite State Machine**, - -- that is, it allows to **embed existing FSM implementations in a master FSM**. - -- FSM hierarchies allow for efficient FSM re-use, **not having to re-invent the wheel every time again** when designing complex processes. - -- - -- ![Workflow Example](..\Presentations\FSM\Dia2.JPG) - -- - -- The above diagram shows a graphical representation of a FSM implementation for a **Task**, which guides a Human towards a Zone, - -- orders him to destroy x targets and account the results. - -- Other examples of ready made FSM could be: - -- - -- * route a plane to a zone flown by a human - -- * detect targets by an AI and report to humans - -- * account for destroyed targets by human players - -- * handle AI infantry to deploy from or embark to a helicopter or airplane or vehicle - -- * let an AI patrol a zone - -- - -- The **MOOSE framework** uses extensively the FSM class and derived FSM\_ classes, - -- because **the goal of MOOSE is to simplify mission design complexity for mission building**. - -- By efficiently utilizing the FSM class and derived classes, MOOSE allows mission designers to quickly build processes. - -- **Ready made FSM-based implementations classes** exist within the MOOSE framework that **can easily be re-used, - -- and tailored** by mission designers through **the implementation of Transition Handlers**. - -- Each of these FSM implementation classes start either with: - -- - -- * an acronym **AI\_**, which indicates an FSM implementation directing **AI controlled** @{GROUP} and/or @{UNIT}. These AI\_ classes derive the @{#FSM_CONTROLLABLE} class. - -- * an acronym **TASK\_**, which indicates an FSM implementation executing a @{TASK} executed by Groups of players. These TASK\_ classes derive the @{#FSM_TASK} class. - -- * an acronym **ACT\_**, which indicates an Sub-FSM implementation, directing **Humans actions** that need to be done in a @{TASK}, seated in a @{CLIENT} (slot) or a @{UNIT} (CA join). These ACT\_ classes derive the @{#FSM_PROCESS} class. - -- - -- ![Transition Rules and Transition Handlers and Event Triggers](..\Presentations\FSM\Dia3.JPG) - -- - -- The FSM class is the base class of all FSM\_ derived classes. It implements the main functionality to define and execute Finite State Machines. - -- The derived FSM\_ classes extend the Finite State Machine functionality to run a workflow process for a specific purpose or component. - -- - -- Finite State Machines have **Transition Rules**, **Transition Handlers** and **Event Triggers**. - -- - -- The **Transition Rules** define the "Process Flow Boundaries", that is, - -- the path that can be followed hopping from state to state upon triggered events. - -- If an event is triggered, and there is no valid path found for that event, - -- an error will be raised and the FSM will stop functioning. - -- - -- The **Transition Handlers** are special methods that can be defined by the mission designer, following a defined syntax. - -- If the FSM object finds a method of such a handler, then the method will be called by the FSM, passing specific parameters. - -- The method can then define its own custom logic to implement the FSM workflow, and to conduct other actions. - -- - -- The **Event Triggers** are methods that are defined by the FSM, which the mission designer can use to implement the workflow. - -- Most of the time, these Event Triggers are used within the Transition Handler methods, so that a workflow is created running through the state machine. - -- - -- As explained above, a FSM supports **Linear State Transitions** and **Hierarchical State Transitions**, and both can be mixed to make a comprehensive FSM implementation. - -- The below documentation has a seperate chapter explaining both transition modes, taking into account the **Transition Rules**, **Transition Handlers** and **Event Triggers**. - -- - -- ## FSM Linear Transitions - -- - -- Linear Transitions are Transition Rules allowing an FSM to transition from one or multiple possible **From** state(s) towards a **To** state upon a Triggered **Event**. - -- The Lineair transition rule evaluation will always be done from the **current state** of the FSM. - -- If no valid Transition Rule can be found in the FSM, the FSM will log an error and stop. - -- - -- ### FSM Transition Rules - -- - -- The FSM has transition rules that it follows and validates, as it walks the process. - -- These rules define when an FSM can transition from a specific state towards an other specific state upon a triggered event. - -- - -- The method @{#FSM.AddTransition}() specifies a new possible Transition Rule for the FSM. - -- - -- The initial state can be defined using the method @{#FSM.SetStartState}(). The default start state of an FSM is "None". - -- - -- Find below an example of a Linear Transition Rule definition for an FSM. - -- - -- local Fsm3Switch = FSM:New() -- #FsmDemo - -- FsmSwitch:SetStartState( "Off" ) - -- FsmSwitch:AddTransition( "Off", "SwitchOn", "On" ) - -- FsmSwitch:AddTransition( "Off", "SwitchMiddle", "Middle" ) - -- FsmSwitch:AddTransition( "On", "SwitchOff", "Off" ) - -- FsmSwitch:AddTransition( "Middle", "SwitchOff", "Off" ) - -- - -- The above code snippet models a 3-way switch Linear Transition: - -- - -- * It can be switched **On** by triggering event **SwitchOn**. - -- * It can be switched to the **Middle** position, by triggering event **SwitchMiddle**. - -- * It can be switched **Off** by triggering event **SwitchOff**. - -- * Note that once the Switch is **On** or **Middle**, it can only be switched **Off**. - -- - -- #### Some additional comments: - -- - -- Note that Linear Transition Rules **can be declared in a few variations**: - -- - -- * The From states can be **a table of strings**, indicating that the transition rule will be valid **if the current state** of the FSM will be **one of the given From states**. - -- * The From state can be a **"*"**, indicating that **the transition rule will always be valid**, regardless of the current state of the FSM. - -- - -- The below code snippet shows how the two last lines can be rewritten and consensed. - -- - -- FsmSwitch:AddTransition( { "On", "Middle" }, "SwitchOff", "Off" ) - -- - -- ### Transition Handling - -- - -- ![Transition Handlers](..\Presentations\FSM\Dia4.JPG) - -- - -- An FSM transitions in **4 moments** when an Event is being triggered and processed. - -- The mission designer can define for each moment specific logic within methods implementations following a defined API syntax. - -- These methods define the flow of the FSM process; because in those methods the FSM Internal Events will be triggered. - -- - -- * To handle **State** transition moments, create methods starting with OnLeave or OnEnter concatenated with the State name. - -- * To handle **Event** transition moments, create methods starting with OnBefore or OnAfter concatenated with the Event name. - -- - -- **The OnLeave and OnBefore transition methods may return false, which will cancel the transition!** - -- - -- Transition Handler methods need to follow the above specified naming convention, but are also passed parameters from the FSM. - -- These parameters are on the correct order: From, Event, To: - -- - -- * From = A string containing the From state. - -- * Event = A string containing the Event name that was triggered. - -- * To = A string containing the To state. - -- - -- On top, each of these methods can have a variable amount of parameters passed. See the example in section [1.1.3](#1.1.3\)-event-triggers). - -- - -- ### Event Triggers - -- - -- ![Event Triggers](..\Presentations\FSM\Dia5.JPG) - -- - -- The FSM creates for each Event two **Event Trigger methods**. - -- There are two modes how Events can be triggered, which is **synchronous** and **asynchronous**: - -- - -- * The method **FSM:Event()** triggers an Event that will be processed **synchronously** or **immediately**. - -- * The method **FSM:__Event( __seconds__ )** triggers an Event that will be processed **asynchronously** over time, waiting __x seconds__. - -- - -- The destinction between these 2 Event Trigger methods are important to understand. An asynchronous call will "log" the Event Trigger to be executed at a later time. - -- Processing will just continue. Synchronous Event Trigger methods are useful to change states of the FSM immediately, but may have a larger processing impact. - -- - -- The following example provides a little demonstration on the difference between synchronous and asynchronous Event Triggering. - -- - -- function FSM:OnAfterEvent( From, Event, To, Amount ) - -- self:T( { Amount = Amount } ) - -- end - -- - -- local Amount = 1 - -- FSM:__Event( 5, Amount ) - -- - -- Amount = Amount + 1 - -- FSM:Event( Text, Amount ) - -- - -- In this example, the **:OnAfterEvent**() Transition Handler implementation will get called when **Event** is being triggered. - -- Before we go into more detail, let's look at the last 4 lines of the example. - -- The last line triggers synchronously the **Event**, and passes Amount as a parameter. - -- The 3rd last line of the example triggers asynchronously **Event**. - -- Event will be processed after 5 seconds, and Amount is given as a parameter. - -- - -- The output of this little code fragment will be: - -- - -- * Amount = 2 - -- * Amount = 2 - -- - -- Because ... When Event was asynchronously processed after 5 seconds, Amount was set to 2. So be careful when processing and passing values and objects in asynchronous processing! - -- - -- ### Linear Transition Example - -- - -- This example is fully implemented in the MOOSE test mission on GITHUB: [FSM-100 - Transition Explanation](https://github.com/FlightControl-Master/MOOSE/blob/master/Moose%20Test%20Missions/FSM%20-%20Finite%20State%20Machine/FSM-100%20-%20Transition%20Explanation/FSM-100%20-%20Transition%20Explanation.lua) - -- - -- It models a unit standing still near Batumi, and flaring every 5 seconds while switching between a Green flare and a Red flare. - -- The purpose of this example is not to show how exciting flaring is, but it demonstrates how a Linear Transition FSM can be build. - -- Have a look at the source code. The source code is also further explained below in this section. - -- - -- The example creates a new FsmDemo object from class FSM. - -- It will set the start state of FsmDemo to state **Green**. - -- Two Linear Transition Rules are created, where upon the event **Switch**, - -- the FsmDemo will transition from state **Green** to **Red** and from **Red** back to **Green**. - -- - -- ![Transition Example](..\Presentations\FSM\Dia6.JPG) - -- - -- local FsmDemo = FSM:New() -- #FsmDemo - -- FsmDemo:SetStartState( "Green" ) - -- FsmDemo:AddTransition( "Green", "Switch", "Red" ) - -- FsmDemo:AddTransition( "Red", "Switch", "Green" ) - -- - -- In the above example, the FsmDemo could flare every 5 seconds a Green or a Red flare into the air. - -- The next code implements this through the event handling method **OnAfterSwitch**. - -- - -- ![Transition Flow](..\Presentations\FSM\Dia7.JPG) - -- - -- function FsmDemo:OnAfterSwitch( From, Event, To, FsmUnit ) - -- self:T( { From, Event, To, FsmUnit } ) - -- - -- if From == "Green" then - -- FsmUnit:Flare(FLARECOLOR.Green) - -- else - -- if From == "Red" then - -- FsmUnit:Flare(FLARECOLOR.Red) - -- end - -- end - -- self:__Switch( 5, FsmUnit ) -- Trigger the next Switch event to happen in 5 seconds. - -- end - -- - -- FsmDemo:__Switch( 5, FsmUnit ) -- Trigger the first Switch event to happen in 5 seconds. - -- - -- The OnAfterSwitch implements a loop. The last line of the code fragment triggers the Switch Event within 5 seconds. - -- Upon the event execution (after 5 seconds), the OnAfterSwitch method is called of FsmDemo (cfr. the double point notation!!! ":"). - -- The OnAfterSwitch method receives from the FSM the 3 transition parameter details ( From, Event, To ), - -- and one additional parameter that was given when the event was triggered, which is in this case the Unit that is used within OnSwitchAfter. - -- - -- function FsmDemo:OnAfterSwitch( From, Event, To, FsmUnit ) - -- - -- For debugging reasons the received parameters are traced within the DCS.log. - -- - -- self:T( { From, Event, To, FsmUnit } ) - -- - -- The method will check if the From state received is either "Green" or "Red" and will flare the respective color from the FsmUnit. - -- - -- if From == "Green" then - -- FsmUnit:Flare(FLARECOLOR.Green) - -- else - -- if From == "Red" then - -- FsmUnit:Flare(FLARECOLOR.Red) - -- end - -- end - -- - -- It is important that the Switch event is again triggered, otherwise, the FsmDemo would stop working after having the first Event being handled. - -- - -- FsmDemo:__Switch( 5, FsmUnit ) -- Trigger the next Switch event to happen in 5 seconds. - -- - -- The below code fragment extends the FsmDemo, demonstrating multiple **From states declared as a table**, adding a **Linear Transition Rule**. - -- The new event **Stop** will cancel the Switching process. - -- The transition for event Stop can be executed if the current state of the FSM is either "Red" or "Green". - -- - -- local FsmDemo = FSM:New() -- #FsmDemo - -- FsmDemo:SetStartState( "Green" ) - -- FsmDemo:AddTransition( "Green", "Switch", "Red" ) - -- FsmDemo:AddTransition( "Red", "Switch", "Green" ) - -- FsmDemo:AddTransition( { "Red", "Green" }, "Stop", "Stopped" ) - -- - -- The transition for event Stop can also be simplified, as any current state of the FSM is valid. - -- - -- FsmDemo:AddTransition( "*", "Stop", "Stopped" ) - -- - -- So... When FsmDemo:Stop() is being triggered, the state of FsmDemo will transition from Red or Green to Stopped. - -- And there is no transition handling method defined for that transition, thus, no new event is being triggered causing the FsmDemo process flow to halt. - -- - -- ## FSM Hierarchical Transitions - -- - -- Hierarchical Transitions allow to re-use readily available and implemented FSMs. - -- This becomes in very useful for mission building, where mission designers build complex processes and workflows, - -- combining smaller FSMs to one single FSM. - -- - -- The FSM can embed **Sub-FSMs** that will execute and return **multiple possible Return (End) States**. - -- Depending upon **which state is returned**, the main FSM can continue the flow **triggering specific events**. - -- - -- The method @{#FSM.AddProcess}() adds a new Sub-FSM to the FSM. - -- - -- === - -- - -- @field #FSM FSM - -- - FSM = { - ClassName = "FSM", - } - - --- Creates a new FSM object. - -- @param #FSM self - -- @return #FSM - function FSM:New( FsmT ) - - -- Inherits from BASE - self = BASE:Inherit( self, BASE:New() ) - - self.options = options or {} - self.options.subs = self.options.subs or {} - self.current = self.options.initial or 'none' - self.Events = {} - self.subs = {} - self.endstates = {} - - self.Scores = {} - - self._StartState = "none" - self._Transitions = {} - self._Processes = {} - self._EndStates = {} - self._Scores = {} - self._EventSchedules = {} - - self.CallScheduler = SCHEDULER:New( self ) - - - return self - end - - - --- Sets the start state of the FSM. - -- @param #FSM self - -- @param #string State A string defining the start state. - function FSM:SetStartState( State ) - - self._StartState = State - self.current = State - end - - - --- Returns the start state of the FSM. - -- @param #FSM self - -- @return #string A string containing the start state. - function FSM:GetStartState() - - return self._StartState or {} - end - - --- Add a new transition rule to the FSM. - -- A transition rule defines when and if the FSM can transition from a state towards another state upon a triggered event. - -- @param #FSM self - -- @param #table From Can contain a string indicating the From state or a table of strings containing multiple From states. - -- @param #string Event The Event name. - -- @param #string To The To state. - function FSM:AddTransition( From, Event, To ) - - local Transition = {} - Transition.From = From - Transition.Event = Event - Transition.To = To - - self:T2( Transition ) - - self._Transitions[Transition] = Transition - self:_eventmap( self.Events, Transition ) - end - - - --- Returns a table of the transition rules defined within the FSM. - -- @return #table - function FSM:GetTransitions() - - return self._Transitions or {} - end - - --- Set the default @{Process} template with key ProcessName providing the ProcessClass and the process object when it is assigned to a @{Controllable} by the task. - -- @param #FSM self - -- @param #table From Can contain a string indicating the From state or a table of strings containing multiple From states. - -- @param #string Event The Event name. - -- @param Core.Fsm#FSM_PROCESS Process An sub-process FSM. - -- @param #table ReturnEvents A table indicating for which returned events of the SubFSM which Event must be triggered in the FSM. - -- @return Core.Fsm#FSM_PROCESS The SubFSM. - function FSM:AddProcess( From, Event, Process, ReturnEvents ) - self:T( { From, Event } ) - - local Sub = {} - Sub.From = From - Sub.Event = Event - Sub.fsm = Process - Sub.StartEvent = "Start" - Sub.ReturnEvents = ReturnEvents - - self._Processes[Sub] = Sub - - self:_submap( self.subs, Sub, nil ) - - self:AddTransition( From, Event, From ) - - return Process - end - - - --- Returns a table of the SubFSM rules defined within the FSM. - -- @return #table - function FSM:GetProcesses() - - return self._Processes or {} - end - - function FSM:GetProcess( From, Event ) - - for ProcessID, Process in pairs( self:GetProcesses() ) do - if Process.From == From and Process.Event == Event then - return Process.fsm - end - end - - error( "Sub-Process from state " .. From .. " with event " .. Event .. " not found!" ) - end - - --- Adds an End state. - function FSM:AddEndState( State ) - - self._EndStates[State] = State - self.endstates[State] = State - end - - --- Returns the End states. - function FSM:GetEndStates() - - return self._EndStates or {} - end - - - --- Adds a score for the FSM to be achieved. - -- @param #FSM self - -- @param #string State is the state of the process when the score needs to be given. (See the relevant state descriptions of the process). - -- @param #string ScoreText is a text describing the score that is given according the status. - -- @param #number Score is a number providing the score of the status. - -- @return #FSM self - function FSM:AddScore( State, ScoreText, Score ) - self:F( { State, ScoreText, Score } ) - - self._Scores[State] = self._Scores[State] or {} - self._Scores[State].ScoreText = ScoreText - self._Scores[State].Score = Score - - return self - end - - --- Adds a score for the FSM_PROCESS to be achieved. - -- @param #FSM self - -- @param #string From is the From State of the main process. - -- @param #string Event is the Event of the main process. - -- @param #string State is the state of the process when the score needs to be given. (See the relevant state descriptions of the process). - -- @param #string ScoreText is a text describing the score that is given according the status. - -- @param #number Score is a number providing the score of the status. - -- @return #FSM self - function FSM:AddScoreProcess( From, Event, State, ScoreText, Score ) - self:F( { From, Event, State, ScoreText, Score } ) - - local Process = self:GetProcess( From, Event ) - - Process._Scores[State] = Process._Scores[State] or {} - Process._Scores[State].ScoreText = ScoreText - Process._Scores[State].Score = Score - - self:T( Process._Scores ) - - return Process - end - - --- Returns a table with the scores defined. - function FSM:GetScores() - - return self._Scores or {} - end - - --- Returns a table with the Subs defined. - function FSM:GetSubs() - - return self.options.subs - end - - - function FSM:LoadCallBacks( CallBackTable ) - - for name, callback in pairs( CallBackTable or {} ) do - self[name] = callback - end - - end - - function FSM:_eventmap( Events, EventStructure ) - - local Event = EventStructure.Event - local __Event = "__" .. EventStructure.Event - self[Event] = self[Event] or self:_create_transition(Event) - self[__Event] = self[__Event] or self:_delayed_transition(Event) - self:T2( "Added methods: " .. Event .. ", " .. __Event ) - Events[Event] = self.Events[Event] or { map = {} } - self:_add_to_map( Events[Event].map, EventStructure ) - - end - - function FSM:_submap( subs, sub, name ) - --self:F( { sub = sub, name = name } ) - subs[sub.From] = subs[sub.From] or {} - subs[sub.From][sub.Event] = subs[sub.From][sub.Event] or {} - - -- Make the reference table weak. - -- setmetatable( subs[sub.From][sub.Event], { __mode = "k" } ) - - subs[sub.From][sub.Event][sub] = {} - subs[sub.From][sub.Event][sub].fsm = sub.fsm - subs[sub.From][sub.Event][sub].StartEvent = sub.StartEvent - subs[sub.From][sub.Event][sub].ReturnEvents = sub.ReturnEvents or {} -- these events need to be given to find the correct continue event ... if none given, the processing will stop. - subs[sub.From][sub.Event][sub].name = name - subs[sub.From][sub.Event][sub].fsmparent = self - end - - - function FSM:_call_handler( handler, params, EventName ) - - local ErrorHandler = function( errmsg ) - - env.info( "Error in SCHEDULER function:" .. errmsg ) - if debug ~= nil then - env.info( debug.traceback() ) - end - - return errmsg - end - if self[handler] then - self:T2( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - local Result, Value = xpcall( function() return self[handler]( self, unpack( params ) ) end, ErrorHandler ) - return Value - end - end - - function FSM._handler( self, EventName, ... ) - - local Can, to = self:can( EventName ) - - if to == "*" then - to = self.current - end - - if Can then - local from = self.current - local params = { from, EventName, to, ... } - - if self.Controllable then - self:T( "FSM Transition for " .. self.Controllable.ControllableName .. " :" .. self.current .. " --> " .. EventName .. " --> " .. to ) - else - self:T( "FSM Transition:" .. self.current .. " --> " .. EventName .. " --> " .. to ) - end - - if ( self:_call_handler("onbefore" .. EventName, params, EventName ) == false ) - or ( self:_call_handler("OnBefore" .. EventName, params, EventName ) == false ) - or ( self:_call_handler("onleave" .. from, params, EventName ) == false ) - or ( self:_call_handler("OnLeave" .. from, params, EventName ) == false ) then - self:T( "Cancel Transition" ) - return false - end - - self.current = to - - local execute = true - - local subtable = self:_gosub( from, EventName ) - for _, sub in pairs( subtable ) do - --if sub.nextevent then - -- self:F2( "nextevent = " .. sub.nextevent ) - -- self[sub.nextevent]( self ) - --end - self:T( "calling sub start event: " .. sub.StartEvent ) - sub.fsm.fsmparent = self - sub.fsm.ReturnEvents = sub.ReturnEvents - sub.fsm[sub.StartEvent]( sub.fsm ) - execute = false - end - - local fsmparent, Event = self:_isendstate( to ) - if fsmparent and Event then - self:F2( { "end state: ", fsmparent, Event } ) - self:_call_handler("onenter" .. to, params, EventName ) - self:_call_handler("OnEnter" .. to, params, EventName ) - self:_call_handler("onafter" .. EventName, params, EventName ) - self:_call_handler("OnAfter" .. EventName, params, EventName ) - self:_call_handler("onstatechange", params, EventName ) - fsmparent[Event]( fsmparent ) - execute = false - end - - if execute then - -- only execute the call if the From state is not equal to the To state! Otherwise this function should never execute! - --if from ~= to then - self:_call_handler("onenter" .. to, params, EventName ) - self:_call_handler("OnEnter" .. to, params, EventName ) - --end - - self:_call_handler("onafter" .. EventName, params, EventName ) - self:_call_handler("OnAfter" .. EventName, params, EventName ) - - self:_call_handler("onstatechange", params, EventName ) - end - else - self:T( "Cannot execute transition." ) - self:T( { From = self.current, Event = EventName, To = to, Can = Can } ) - end - - return nil - end - - function FSM:_delayed_transition( EventName ) - return function( self, DelaySeconds, ... ) - self:T2( "Delayed Event: " .. EventName ) - local CallID = 0 - if DelaySeconds ~= nil then - if DelaySeconds < 0 then -- Only call the event ONCE! - DelaySeconds = math.abs( DelaySeconds ) - if not self._EventSchedules[EventName] then - CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) - self._EventSchedules[EventName] = CallID - else - -- reschedule - end - else - CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) - end - else - error( "FSM: An asynchronous event trigger requires a DelaySeconds parameter!!! This can be positive or negative! Sorry, but will not process this." ) - end - self:T2( { CallID = CallID } ) - end - end - - function FSM:_create_transition( EventName ) - return function( self, ... ) return self._handler( self, EventName , ... ) end - end - - function FSM:_gosub( ParentFrom, ParentEvent ) - local fsmtable = {} - if self.subs[ParentFrom] and self.subs[ParentFrom][ParentEvent] then - self:T( { ParentFrom, ParentEvent, self.subs[ParentFrom], self.subs[ParentFrom][ParentEvent] } ) - return self.subs[ParentFrom][ParentEvent] - else - return {} - end - end - - function FSM:_isendstate( Current ) - local FSMParent = self.fsmparent - if FSMParent and self.endstates[Current] then - self:T( { state = Current, endstates = self.endstates, endstate = self.endstates[Current] } ) - FSMParent.current = Current - local ParentFrom = FSMParent.current - self:T( ParentFrom ) - self:T( self.ReturnEvents ) - local Event = self.ReturnEvents[Current] - self:T( { ParentFrom, Event, self.ReturnEvents } ) - if Event then - return FSMParent, Event - else - self:T( { "Could not find parent event name for state ", ParentFrom } ) - end - end - - return nil - end - - function FSM:_add_to_map( Map, Event ) - self:F3( { Map, Event } ) - if type(Event.From) == 'string' then - Map[Event.From] = Event.To - else - for _, From in ipairs(Event.From) do - Map[From] = Event.To - end - end - self:T3( { Map, Event } ) - end - - function FSM:GetState() - return self.current - end - - - function FSM:Is( State ) - return self.current == State - end - - function FSM:is(state) - return self.current == state - end - - function FSM:can(e) - local Event = self.Events[e] - self:F3( { self.current, Event } ) - local To = Event and Event.map[self.current] or Event.map['*'] - return To ~= nil, To - end - - function FSM:cannot(e) - return not self:can(e) - end - -end - -do -- FSM_CONTROLLABLE - - --- @type FSM_CONTROLLABLE - -- @field Wrapper.Controllable#CONTROLLABLE Controllable - -- @extends Core.Fsm#FSM - - --- # FSM_CONTROLLABLE, extends @{#FSM} - -- - -- FSM_CONTROLLABLE class models Finite State Machines for @{Controllable}s, which are @{Group}s, @{Unit}s, @{Client}s. - -- - -- === - -- - -- @field #FSM_CONTROLLABLE FSM_CONTROLLABLE - -- - FSM_CONTROLLABLE = { - ClassName = "FSM_CONTROLLABLE", - } - - --- Creates a new FSM_CONTROLLABLE object. - -- @param #FSM_CONTROLLABLE self - -- @param #table FSMT Finite State Machine Table - -- @param Wrapper.Controllable#CONTROLLABLE Controllable (optional) The CONTROLLABLE object that the FSM_CONTROLLABLE governs. - -- @return #FSM_CONTROLLABLE - function FSM_CONTROLLABLE:New( FSMT, Controllable ) - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM:New( FSMT ) ) -- Core.Fsm#FSM_CONTROLLABLE - - if Controllable then - self:SetControllable( Controllable ) - end - - self:AddTransition( "*", "Stop", "Stopped" ) - - --- OnBefore Transition Handler for Event Stop. - -- @function [parent=#FSM_CONTROLLABLE] OnBeforeStop - -- @param #FSM_CONTROLLABLE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Stop. - -- @function [parent=#FSM_CONTROLLABLE] OnAfterStop - -- @param #FSM_CONTROLLABLE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Stop. - -- @function [parent=#FSM_CONTROLLABLE] Stop - -- @param #FSM_CONTROLLABLE self - - --- Asynchronous Event Trigger for Event Stop. - -- @function [parent=#FSM_CONTROLLABLE] __Stop - -- @param #FSM_CONTROLLABLE self - -- @param #number Delay The delay in seconds. - - --- OnLeave Transition Handler for State Stopped. - -- @function [parent=#FSM_CONTROLLABLE] OnLeaveStopped - -- @param #FSM_CONTROLLABLE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnEnter Transition Handler for State Stopped. - -- @function [parent=#FSM_CONTROLLABLE] OnEnterStopped - -- @param #FSM_CONTROLLABLE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - return self - end - - --- OnAfter Transition Handler for Event Stop. - -- @function [parent=#FSM_CONTROLLABLE] OnAfterStop - -- @param #FSM_CONTROLLABLE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - function FSM_CONTROLLABLE:OnAfterStop(Controllable,From,Event,To) - - -- Clear all pending schedules - self.CallScheduler:Clear() - end - - --- Sets the CONTROLLABLE object that the FSM_CONTROLLABLE governs. - -- @param #FSM_CONTROLLABLE self - -- @param Wrapper.Controllable#CONTROLLABLE FSMControllable - -- @return #FSM_CONTROLLABLE - function FSM_CONTROLLABLE:SetControllable( FSMControllable ) - --self:F( FSMControllable:GetName() ) - self.Controllable = FSMControllable - end - - --- Gets the CONTROLLABLE object that the FSM_CONTROLLABLE governs. - -- @param #FSM_CONTROLLABLE self - -- @return Wrapper.Controllable#CONTROLLABLE - function FSM_CONTROLLABLE:GetControllable() - return self.Controllable - end - - function FSM_CONTROLLABLE:_call_handler( handler, params, EventName ) - - local ErrorHandler = function( errmsg ) - - env.info( "Error in SCHEDULER function:" .. errmsg ) - if debug ~= nil then - env.info( debug.traceback() ) - end - - return errmsg - end - - if self[handler] then - self:F3( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - local Result, Value = xpcall( function() return self[handler]( self, self.Controllable, unpack( params ) ) end, ErrorHandler ) - return Value - --return self[handler]( self, self.Controllable, unpack( params ) ) - end - end - -end - -do -- FSM_PROCESS - - --- @type FSM_PROCESS - -- @field Tasking.Task#TASK Task - -- @extends Core.Fsm#FSM_CONTROLLABLE - - - --- # FSM_PROCESS, extends @{#FSM} - -- - -- FSM_PROCESS class models Finite State Machines for @{Task} actions, which control @{Client}s. - -- - -- === - -- - -- @field #FSM_PROCESS FSM_PROCESS - -- - FSM_PROCESS = { - ClassName = "FSM_PROCESS", - } - - --- Creates a new FSM_PROCESS object. - -- @param #FSM_PROCESS self - -- @return #FSM_PROCESS - function FSM_PROCESS:New( Controllable, Task ) - - local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- Core.Fsm#FSM_PROCESS - - --self:F( Controllable ) - - self:Assign( Controllable, Task ) - - return self - end - - function FSM_PROCESS:Init( FsmProcess ) - self:T( "No Initialisation" ) - end - - function FSM_PROCESS:_call_handler( handler, params, EventName ) - - local ErrorHandler = function( errmsg ) - - env.info( "Error in FSM_PROCESS call handler:" .. errmsg ) - if debug ~= nil then - env.info( debug.traceback() ) - end - - return errmsg - end - - if self[handler] then - self:F3( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - local Result, Value = xpcall( function() return self[handler]( self, self.Controllable, self.Task, unpack( params ) ) end, ErrorHandler ) - return Value - --return self[handler]( self, self.Controllable, unpack( params ) ) - end - end - - --- Creates a new FSM_PROCESS object based on this FSM_PROCESS. - -- @param #FSM_PROCESS self - -- @return #FSM_PROCESS - function FSM_PROCESS:Copy( Controllable, Task ) - self:T( { self:GetClassNameAndID() } ) - - - local NewFsm = self:New( Controllable, Task ) -- Core.Fsm#FSM_PROCESS - - NewFsm:Assign( Controllable, Task ) - - -- Polymorphic call to initialize the new FSM_PROCESS based on self FSM_PROCESS - NewFsm:Init( self ) - - -- Set Start State - NewFsm:SetStartState( self:GetStartState() ) - - -- Copy Transitions - for TransitionID, Transition in pairs( self:GetTransitions() ) do - NewFsm:AddTransition( Transition.From, Transition.Event, Transition.To ) - end - - -- Copy Processes - for ProcessID, Process in pairs( self:GetProcesses() ) do - --self:E( { Process:GetName() } ) - local FsmProcess = NewFsm:AddProcess( Process.From, Process.Event, Process.fsm:Copy( Controllable, Task ), Process.ReturnEvents ) - end - - -- Copy End States - for EndStateID, EndState in pairs( self:GetEndStates() ) do - self:T( EndState ) - NewFsm:AddEndState( EndState ) - end - - -- Copy the score tables - for ScoreID, Score in pairs( self:GetScores() ) do - self:T( Score ) - NewFsm:AddScore( ScoreID, Score.ScoreText, Score.Score ) - end - - return NewFsm - end - - --- Removes an FSM_PROCESS object. - -- @param #FSM_PROCESS self - -- @return #FSM_PROCESS - function FSM_PROCESS:Remove() - self:F( { self:GetClassNameAndID() } ) - - self:F( "Clearing Schedules" ) - self.CallScheduler:Clear() - - -- Copy Processes - for ProcessID, Process in pairs( self:GetProcesses() ) do - if Process.fsm then - Process.fsm:Remove() - Process.fsm = nil - end - end - - return self - end - - --- Sets the task of the process. - -- @param #FSM_PROCESS self - -- @param Tasking.Task#TASK Task - -- @return #FSM_PROCESS - function FSM_PROCESS:SetTask( Task ) - - self.Task = Task - - return self - end - - --- Gets the task of the process. - -- @param #FSM_PROCESS self - -- @return Tasking.Task#TASK - function FSM_PROCESS:GetTask() - - return self.Task - end - - --- Gets the mission of the process. - -- @param #FSM_PROCESS self - -- @return Tasking.Mission#MISSION - function FSM_PROCESS:GetMission() - - return self.Task.Mission - end - - --- Gets the mission of the process. - -- @param #FSM_PROCESS self - -- @return Tasking.CommandCenter#COMMANDCENTER - function FSM_PROCESS:GetCommandCenter() - - return self:GetTask():GetMission():GetCommandCenter() - end - --- TODO: Need to check and fix that an FSM_PROCESS is only for a UNIT. Not for a GROUP. - - --- Send a message of the @{Task} to the Group of the Unit. --- @param #FSM_PROCESS self -function FSM_PROCESS:Message( Message ) - self:F( { Message = Message } ) - - local CC = self:GetCommandCenter() - local TaskGroup = self.Controllable:GetGroup() - - local PlayerName = self.Controllable:GetPlayerName() -- Only for a unit - PlayerName = PlayerName and " (" .. PlayerName .. ")" or "" -- If PlayerName is nil, then keep it nil, otherwise add brackets. - local Callsign = self.Controllable:GetCallsign() - local Prefix = Callsign and " @ " .. Callsign .. PlayerName or "" - - Message = Prefix .. ": " .. Message - CC:MessageToGroup( Message, TaskGroup ) -end - - - - - --- Assign the process to a @{Unit} and activate the process. - -- @param #FSM_PROCESS self - -- @param Task.Tasking#TASK Task - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @return #FSM_PROCESS self - function FSM_PROCESS:Assign( ProcessUnit, Task ) - --self:T( { Task:GetName(), ProcessUnit:GetName() } ) - - self:SetControllable( ProcessUnit ) - self:SetTask( Task ) - - --self.ProcessGroup = ProcessUnit:GetGroup() - - return self - end - - function FSM_PROCESS:onenterAssigned( ProcessUnit ) - self:T( "Assign" ) - - self.Task:Assign() - end - - function FSM_PROCESS:onenterFailed( ProcessUnit ) - self:T( "Failed" ) - - self.Task:Fail() - end - - - --- StateMachine callback function for a FSM_PROCESS - -- @param #FSM_PROCESS self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function FSM_PROCESS:onstatechange( ProcessUnit, Task, From, Event, To, Dummy ) - self:T( { ProcessUnit:GetName(), From, Event, To, Dummy, self:IsTrace() } ) - - if self:IsTrace() then - --MESSAGE:New( "@ Process " .. self:GetClassNameAndID() .. " : " .. Event .. " changed to state " .. To, 2 ):ToAll() - end - - self:T( { Scores = self._Scores, To = To } ) - -- TODO: This needs to be reworked with a callback functions allocated within Task, and set within the mission script from the Task Objects... - if self._Scores[To] then - - local Task = self.Task - local Scoring = Task:GetScoring() - if Scoring then - Scoring:_AddMissionTaskScore( Task.Mission, ProcessUnit, self._Scores[To].ScoreText, self._Scores[To].Score ) - end - end - end - -end - -do -- FSM_TASK - - --- FSM_TASK class - -- @type FSM_TASK - -- @field Tasking.Task#TASK Task - -- @extends #FSM - - --- # FSM_TASK, extends @{#FSM} - -- - -- FSM_TASK class models Finite State Machines for @{Task}s. - -- - -- === - -- - -- @field #FSM_TASK FSM_TASK - -- - FSM_TASK = { - ClassName = "FSM_TASK", - } - - --- Creates a new FSM_TASK object. - -- @param #FSM_TASK self - -- @param #table FSMT - -- @param Tasking.Task#TASK Task - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #FSM_TASK - function FSM_TASK:New( FSMT ) - - local self = BASE:Inherit( self, FSM_CONTROLLABLE:New( FSMT ) ) -- Core.Fsm#FSM_TASK - - self["onstatechange"] = self.OnStateChange - - return self - end - - function FSM_TASK:_call_handler( handler, params, EventName ) - if self[handler] then - self:T( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - return self[handler]( self, unpack( params ) ) - end - end - -end -- FSM_TASK - -do -- FSM_SET - - --- FSM_SET class - -- @type FSM_SET - -- @field Core.Set#SET_BASE Set - -- @extends Core.Fsm#FSM - - - --- # FSM_SET, extends @{#FSM} - -- - -- FSM_SET class models Finite State Machines for @{Set}s. Note that these FSMs control multiple objects!!! So State concerns here - -- for multiple objects or the position of the state machine in the process. - -- - -- === - -- - -- @field #FSM_SET FSM_SET - -- - FSM_SET = { - ClassName = "FSM_SET", - } - - --- Creates a new FSM_SET object. - -- @param #FSM_SET self - -- @param #table FSMT Finite State Machine Table - -- @param Set_SET_BASE FSMSet (optional) The Set object that the FSM_SET governs. - -- @return #FSM_SET - function FSM_SET:New( FSMSet ) - - -- Inherits from BASE - self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_SET - - if FSMSet then - self:Set( FSMSet ) - end - - return self - end - - --- Sets the SET_BASE object that the FSM_SET governs. - -- @param #FSM_SET self - -- @param Core.Set#SET_BASE FSMSet - -- @return #FSM_SET - function FSM_SET:Set( FSMSet ) - self:F( FSMSet ) - self.Set = FSMSet - end - - --- Gets the SET_BASE object that the FSM_SET governs. - -- @param #FSM_SET self - -- @return Core.Set#SET_BASE - function FSM_SET:Get() - return self.Controllable - end - - function FSM_SET:_call_handler( handler, params, EventName ) - if self[handler] then - self:T( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - return self[handler]( self, self.Set, unpack( params ) ) - end - end - -end -- FSM_SET - ---- **Core** -- The RADIO Module is responsible for everything that is related to radio transmission and you can hear in DCS, be it TACAN beacons, Radio transmissions... --- --- ![Banner Image](..\Presentations\RADIO\Dia1.JPG) --- --- === --- --- The Radio contains 2 classes : RADIO and BEACON --- --- What are radio communications in DCS ? --- --- * Radio transmissions consist of **sound files** that are broadcasted on a specific **frequency** (e.g. 115MHz) and **modulation** (e.g. AM), --- * They can be **subtitled** for a specific **duration**, the **power** in Watts of the transmiter's antenna can be set, and the transmission can be **looped**. --- --- How to supply DCS my own Sound Files ? --- --- * Your sound files need to be encoded in **.ogg** or .wav, --- * Your sound files should be **as tiny as possible**. It is suggested you encode in .ogg with low bitrate and sampling settings, --- * They need to be added in .\l10n\DEFAULT\ in you .miz file (wich can be decompressed like a .zip file), --- * For simplicty sake, you can **let DCS' Mission Editor add the file** itself, by creating a new Trigger with the action "Sound to Country", and choosing your sound file and a country you don't use in your mission. --- --- Due to weird DCS quirks, **radio communications behave differently** if sent by a @{Unit#UNIT} or a @{Group#GROUP} or by any other @{Positionable#POSITIONABLE} --- --- * If the transmitter is a @{Unit#UNIT} or a @{Group#GROUP}, DCS will set the power of the transmission automatically, --- * If the transmitter is any other @{Positionable#POSITIONABLE}, the transmisison can't be subtitled or looped. --- --- Note that obviously, the **frequency** and the **modulation** of the transmission are important only if the players are piloting an **Advanced System Modelling** enabled aircraft, --- like the A10C or the Mirage 2000C. They will **hear the transmission** if they are tuned on the **right frequency and modulation** (and if they are close enough - more on that below). --- If a FC3 airacraft is used, it will **hear every communication, whatever the frequency and the modulation** is set to. The same is true for TACAN beacons. If your aircaft isn't compatible, --- you won't hear/be able to use the TACAN beacon informations. --- --- === --- --- ### Author: Hugues "Grey_Echo" Bousquet --- --- @module Radio - - ---- # RADIO class, extends @{Base#BASE} --- --- ## RADIO usage --- --- There are 3 steps to a successful radio transmission. --- --- * First, you need to **"add a @{#RADIO} object** to your @{Positionable#POSITIONABLE}. This is done using the @{Positionable#POSITIONABLE.GetRadio}() function, --- * Then, you will **set the relevant parameters** to the transmission (see below), --- * When done, you can actually **broadcast the transmission** (i.e. play the sound) with the @{RADIO.Broadcast}() function. --- --- Methods to set relevant parameters for both a @{Unit#UNIT} or a @{Group#GROUP} or any other @{Positionable#POSITIONABLE} --- --- * @{#RADIO.SetFileName}() : Sets the file name of your sound file (e.g. "Noise.ogg"), --- * @{#RADIO.SetFrequency}() : Sets the frequency of your transmission. --- * @{#RADIO.SetModulation}() : Sets the modulation of your transmission. --- * @{#RADIO.SetLoop}() : Choose if you want the transmission to be looped. If you need your transmission to be looped, you might need a @{#BEACON} instead... --- --- Additional Methods to set relevant parameters if the transmiter is a @{Unit#UNIT} or a @{Group#GROUP} --- --- * @{#RADIO.SetSubtitle}() : Set both the subtitle and its duration, --- * @{#RADIO.NewUnitTransmission}() : Shortcut to set all the relevant parameters in one method call --- --- Additional Methods to set relevant parameters if the transmiter is any other @{Positionable#POSITIONABLE} --- --- * @{#RADIO.SetPower}() : Sets the power of the antenna in Watts --- * @{#RADIO.NewGenericTransmission}() : Shortcut to set all the relevant parameters in one method call --- --- What is this power thing ? --- --- * If your transmission is sent by a @{Positionable#POSITIONABLE} other than a @{Unit#UNIT} or a @{Group#GROUP}, you can set the power of the antenna, --- * Otherwise, DCS sets it automatically, depending on what's available on your Unit, --- * If the player gets **too far** from the transmiter, or if the antenna is **too weak**, the transmission will **fade** and **become noisyer**, --- * This an automated DCS calculation you have no say on, --- * For reference, a standard VOR station has a 100W antenna, a standard AA TACAN has a 120W antenna, and civilian ATC's antenna usually range between 300 and 500W, --- * Note that if the transmission has a subtitle, it will be readable, regardless of the quality of the transmission. --- --- @type RADIO --- @field Positionable#POSITIONABLE Positionable The transmiter --- @field #string FileName Name of the sound file --- @field #number Frequency Frequency of the transmission in Hz --- @field #number Modulation Modulation of the transmission (either radio.modulation.AM or radio.modulation.FM) --- @field #string Subtitle Subtitle of the transmission --- @field #number SubtitleDuration Duration of the Subtitle in seconds --- @field #number Power Power of the antenna is Watts --- @field #boolean Loop (default true) --- @extends Core.Base#BASE -RADIO = { - ClassName = "RADIO", - FileName = "", - Frequency = 0, - Modulation = radio.modulation.AM, - Subtitle = "", - SubtitleDuration = 0, - Power = 100, - Loop = true, -} - ---- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast --- If you want to create a RADIO, you probably should use @{Positionable#POSITIONABLE.GetRadio}() instead --- @param #RADIO self --- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. --- @return #RADIO Radio --- @return #nil If Positionable is invalid -function RADIO:New(Positionable) - local self = BASE:Inherit( self, BASE:New() ) -- Core.Radio#RADIO - - self.Loop = true -- default Loop to true (not sure the above RADIO definition actually is working) - self:F(Positionable) - - if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid - self.Positionable = Positionable - return self - end - - self:E({"The passed positionable is invalid, no RADIO created", Positionable}) - return nil -end - ---- Check validity of the filename passed and sets RADIO.FileName --- @param #RADIO self --- @param #string FileName File name of the sound file (i.e. "Noise.ogg") --- @return #RADIO self -function RADIO:SetFileName(FileName) - self:F2(FileName) - - if type(FileName) == "string" then - if FileName:find(".ogg") or FileName:find(".wav") then - if not FileName:find("l10n/DEFAULT/") then - FileName = "l10n/DEFAULT/" .. FileName - end - self.FileName = FileName - return self - end - end - - self:E({"File name invalid. Maybe something wrong with the extension ?", self.FileName}) - return self -end - ---- Check validity of the frequency passed and sets RADIO.Frequency --- @param #RADIO self --- @param #number Frequency in MHz (Ranges allowed for radio transmissions in DCS : 30-88 / 108-152 / 225-400MHz) --- @return #RADIO self -function RADIO:SetFrequency(Frequency) - self:F2(Frequency) - if type(Frequency) == "number" then - -- If frequency is in range - if (Frequency >= 30 and Frequency < 88) or (Frequency >= 108 and Frequency < 152) or (Frequency >= 225 and Frequency < 400) then - self.Frequency = Frequency * 1000000 -- Conversion in Hz - -- If the RADIO is attached to a UNIT or a GROUP, we need to send the DCS Command "SetFrequency" to change the UNIT or GROUP frequency - if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self.Positionable:SetCommand({ - id = "SetFrequency", - params = { - frequency = self.Frequency, - modulation = self.Modulation, - } - }) - end - return self - end - end - self:E({"Frequency is outside of DCS Frequency ranges (30-80, 108-152, 225-400). Frequency unchanged.", self.Frequency}) - return self -end - ---- Check validity of the frequency passed and sets RADIO.Modulation --- @param #RADIO self --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @return #RADIO self -function RADIO:SetModulation(Modulation) - self:F2(Modulation) - if type(Modulation) == "number" then - if Modulation == radio.modulation.AM or Modulation == radio.modulation.FM then --TODO Maybe make this future proof if ED decides to add an other modulation ? - self.Modulation = Modulation - return self - end - end - self:E({"Modulation is invalid. Use DCS's enum radio.modulation. Modulation unchanged.", self.Modulation}) - return self -end - ---- Check validity of the power passed and sets RADIO.Power --- @param #RADIO self --- @param #number Power in W --- @return #RADIO self -function RADIO:SetPower(Power) - self:F2(Power) - if type(Power) == "number" then - self.Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that - return self - end - self:E({"Power is invalid. Power unchanged.", self.Power}) - return self -end - ---- Check validity of the loop passed and sets RADIO.Loop --- @param #RADIO self --- @param #boolean Loop --- @return #RADIO self --- @usage -function RADIO:SetLoop(Loop) - self:F2(Loop) - if type(Loop) == "boolean" then - self.Loop = Loop - return self - end - self:E({"Loop is invalid. Loop unchanged.", self.Loop}) - return self -end - ---- Check validity of the subtitle and the subtitleDuration passed and sets RADIO.subtitle and RADIO.subtitleDuration --- Both parameters are mandatory, since it wouldn't make much sense to change the Subtitle and not its duration --- @param #RADIO self --- @param #string Subtitle --- @param #number SubtitleDuration in s --- @return #RADIO self --- @usage --- -- create the broadcaster and attaches it a RADIO --- local MyUnit = UNIT:FindByName("MyUnit") --- local MyUnitRadio = MyUnit:GetRadio() --- --- -- add a subtitle for the next transmission, which will be up for 10s --- MyUnitRadio:SetSubtitle("My Subtitle, 10) -function RADIO:SetSubtitle(Subtitle, SubtitleDuration) - self:F2({Subtitle, SubtitleDuration}) - if type(Subtitle) == "string" then - self.Subtitle = Subtitle - else - self.Subtitle = "" - self:E({"Subtitle is invalid. Subtitle reset.", self.Subtitle}) - end - if type(SubtitleDuration) == "number" then - if math.floor(math.abs(SubtitleDuration)) == SubtitleDuration then - self.SubtitleDuration = SubtitleDuration - return self - end - end - self.SubtitleDuration = 0 - self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) -end - ---- Create a new transmission, that is to say, populate the RADIO with relevant data --- In this function the data is especially relevant if the broadcaster is anything but a UNIT or a GROUP, --- but it will work with a UNIT or a GROUP anyway. --- Only the #RADIO and the Filename are mandatory --- @param #RADIO self --- @param #string FileName --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #number Power in W --- @return #RADIO self -function RADIO:NewGenericTransmission(FileName, Frequency, Modulation, Power, Loop) - self:F({FileName, Frequency, Modulation, Power}) - - self:SetFileName(FileName) - if Frequency then self:SetFrequency(Frequency) end - if Modulation then self:SetModulation(Modulation) end - if Power then self:SetPower(Power) end - if Loop then self:SetLoop(Loop) end - - return self -end - - ---- Create a new transmission, that is to say, populate the RADIO with relevant data --- In this function the data is especially relevant if the broadcaster is a UNIT or a GROUP, --- but it will work for any @{Positionable#POSITIONABLE}. --- Only the RADIO and the Filename are mandatory. --- @param #RADIO self --- @param #string FileName --- @param #string Subtitle --- @param #number SubtitleDuration in s --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #boolean Loop --- @return #RADIO self -function RADIO:NewUnitTransmission(FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop) - self:F({FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop}) - - self:SetFileName(FileName) - if Subtitle then self:SetSubtitle(Subtitle) end - if SubtitleDuration then self:SetSubtitleDuration(SubtitleDuration) end - if Frequency then self:SetFrequency(Frequency) end - if Modulation then self:SetModulation(Modulation) end - if Loop then self:SetLoop(Loop) end - - return self -end - ---- Actually Broadcast the transmission --- * The Radio has to be populated with the new transmission before broadcasting. --- * Please use RADIO setters or either @{Radio#RADIO.NewGenericTransmission} or @{Radio#RADIO.NewUnitTransmission} --- * This class is in fact pretty smart, it determines the right DCS function to use depending on the type of POSITIONABLE --- * If the POSITIONABLE is not a UNIT or a GROUP, we use the generic (but limited) trigger.action.radioTransmission() --- * If the POSITIONABLE is a UNIT or a GROUP, we use the "TransmitMessage" Command --- * If your POSITIONABLE is a UNIT or a GROUP, the Power is ignored. --- * If your POSITIONABLE is not a UNIT or a GROUP, the Subtitle, SubtitleDuration are ignored --- @param #RADIO self --- @return #RADIO self -function RADIO:Broadcast() - self:F() - - -- If the POSITIONABLE is actually a UNIT or a GROUP, use the more complicated DCS command system - if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self:T2("Broadcasting from a UNIT or a GROUP") - self.Positionable:SetCommand({ - id = "TransmitMessage", - params = { - file = self.FileName, - duration = self.SubtitleDuration, - subtitle = self.Subtitle, - loop = self.Loop, - } - }) - else - -- If the POSITIONABLE is anything else, we revert to the general singleton function - -- I need to give it a unique name, so that the transmission can be stopped later. I use the class ID - self:T2("Broadcasting from a POSITIONABLE") - trigger.action.radioTransmission(self.FileName, self.Positionable:GetPositionVec3(), self.Modulation, self.Loop, self.Frequency, self.Power, tostring(self.ID)) - end - return self -end - ---- Stops a transmission --- This function is especially usefull to stop the broadcast of looped transmissions --- @param #RADIO self --- @return #RADIO self -function RADIO:StopBroadcast() - self:F() - -- If the POSITIONABLE is a UNIT or a GROUP, stop the transmission with the DCS "StopTransmission" command - if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self.Positionable:SetCommand({ - id = "StopTransmission", - params = {} - }) - else - -- Else, we use the appropriate singleton funciton - trigger.action.stopRadioTransmission(tostring(self.ID)) - end - return self -end - - ---- # BEACON class, extends @{Base#BASE} --- --- After attaching a @{#BEACON} to your @{Positionable#POSITIONABLE}, you need to select the right function to activate the kind of beacon you want. --- There are two types of BEACONs available : the AA TACAN Beacon and the general purpose Radio Beacon. --- Note that in both case, you can set an optional parameter : the `BeaconDuration`. This can be very usefull to simulate the battery time if your BEACON is --- attach to a cargo crate, for exemple. --- --- ## AA TACAN Beacon usage --- --- This beacon only works with airborne @{Unit#UNIT} or a @{Group#GROUP}. Use @{#BEACON:AATACAN}() to set the beacon parameters and start the beacon. --- Use @#BEACON:StopAATACAN}() to stop it. --- --- ## General Purpose Radio Beacon usage --- --- This beacon will work with any @{Positionable#POSITIONABLE}, but **it won't follow the @{Positionable#POSITIONABLE}** ! This means that you should only use it with --- @{Positionable#POSITIONABLE} that don't move, or move very slowly. Use @{#BEACON:RadioBeacon}() to set the beacon parameters and start the beacon. --- Use @{#BEACON:StopRadioBeacon}() to stop it. --- --- @type BEACON --- @extends Core.Base#BASE -BEACON = { - ClassName = "BEACON", -} - ---- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.AATACAN} or @{#BEACON.Generic} --- If you want to create a BEACON, you probably should use @{Positionable#POSITIONABLE.GetBeacon}() instead. --- @param #BEACON self --- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. --- @return #BEACON Beacon --- @return #nil If Positionable is invalid -function BEACON:New(Positionable) - local self = BASE:Inherit(self, BASE:New()) - - self:F(Positionable) - - if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid - self.Positionable = Positionable - return self - end - - self:E({"The passed positionable is invalid, no BEACON created", Positionable}) - return nil -end - - ---- Converts a TACAN Channel/Mode couple into a frequency in Hz --- @param #BEACON self --- @param #number TACANChannel --- @param #string TACANMode --- @return #number Frequecy --- @return #nil if parameters are invalid -function BEACON:_TACANToFrequency(TACANChannel, TACANMode) - self:F3({TACANChannel, TACANMode}) - - if type(TACANChannel) ~= "number" then - if TACANMode ~= "X" and TACANMode ~= "Y" then - return nil -- error in arguments - end - end - --- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. --- I have no idea what it does but it seems to work - local A = 1151 -- 'X', channel >= 64 - local B = 64 -- channel >= 64 - - if TACANChannel < 64 then - B = 1 - end - - if TACANMode == 'Y' then - A = 1025 - if TACANChannel < 64 then - A = 1088 - end - else -- 'X' - if TACANChannel < 64 then - A = 962 - end - end - - return (A + TACANChannel - B) * 1000000 -end - - ---- Activates a TACAN BEACON on an Aircraft. --- @param #BEACON self --- @param #number TACANChannel (the "10" part in "10Y"). Note that AA TACAN are only available on Y Channels --- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon --- @param #boolean Bearing Can the BEACON be homed on ? --- @param #number BeaconDuration How long will the beacon last in seconds. Omit for forever. --- @return #BEACON self --- @usage --- -- Let's create a TACAN Beacon for a tanker --- local myUnit = UNIT:FindByName("MyUnit") --- local myBeacon = myUnit:GetBeacon() -- Creates the beacon --- --- myBeacon:AATACAN(20, "TEXACO", true) -- Activate the beacon -function BEACON:AATACAN(TACANChannel, Message, Bearing, BeaconDuration) - self:F({TACANChannel, Message, Bearing, BeaconDuration}) - - local IsValid = true - - if not self.Positionable:IsAir() then - self:E({"The POSITIONABLE you want to attach the AA Tacan Beacon is not an aircraft ! The BEACON is not emitting", self.Positionable}) - IsValid = false - end - - local Frequency = self:_TACANToFrequency(TACANChannel, "Y") - if not Frequency then - self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) - IsValid = false - end - - -- I'm using the beacon type 4 (BEACON_TYPE_TACAN). For System, I'm using 5 (TACAN_TANKER_MODE_Y) if the bearing shows its bearing - -- or 14 (TACAN_AA_MODE_Y) if it does not - local System - if Bearing then - System = 5 - else - System = 14 - end - - if IsValid then -- Starts the BEACON - self:T2({"AA TACAN BEACON started !"}) - self.Positionable:SetCommand({ - id = "ActivateBeacon", - params = { - type = 4, - system = System, - callsign = Message, - frequency = Frequency, - } - }) - - if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD - SCHEDULER:New( nil, - function() - self:StopAATACAN() - end, {}, BeaconDuration) - end - end - - return self -end - ---- Stops the AA TACAN BEACON --- @param #BEACON self --- @return #BEACON self -function BEACON:StopAATACAN() - self:F() - if not self.Positionable then - self:E({"Start the beacon first before stoping it !"}) - else - self.Positionable:SetCommand({ - id = 'DeactivateBeacon', - params = { - } - }) - end -end - - ---- Activates a general pupose Radio Beacon --- This uses the very generic singleton function "trigger.action.radioTransmission()" provided by DCS to broadcast a sound file on a specific frequency. --- Although any frequency could be used, only 2 DCS Modules can home on radio beacons at the time of writing : the Huey and the Mi-8. --- They can home in on these specific frequencies : --- * **Mi8** --- * R-828 -> 20-60MHz --- * ARKUD -> 100-150MHz (canal 1 : 114166, canal 2 : 114333, canal 3 : 114583, canal 4 : 121500, canal 5 : 123100, canal 6 : 124100) AM --- * ARK9 -> 150-1300KHz --- * **Huey** --- * AN/ARC-131 -> 30-76 Mhz FM --- @param #BEACON self --- @param #string FileName The name of the audio file --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #number Power in W --- @param #number BeaconDuration How long will the beacon last in seconds. Omit for forever. --- @return #BEACON self --- @usage --- -- Let's create a beacon for a unit in distress. --- -- Frequency will be 40MHz FM (home-able by a Huey's AN/ARC-131) --- -- The beacon they use is battery-powered, and only lasts for 5 min --- local UnitInDistress = UNIT:FindByName("Unit1") --- local UnitBeacon = UnitInDistress:GetBeacon() --- --- -- Set the beacon and start it --- UnitBeacon:RadioBeacon("MySoundFileSOS.ogg", 40, radio.modulation.FM, 20, 5*60) -function BEACON:RadioBeacon(FileName, Frequency, Modulation, Power, BeaconDuration) - self:F({FileName, Frequency, Modulation, Power, BeaconDuration}) - local IsValid = false - - -- Check the filename - if type(FileName) == "string" then - if FileName:find(".ogg") or FileName:find(".wav") then - if not FileName:find("l10n/DEFAULT/") then - FileName = "l10n/DEFAULT/" .. FileName - end - IsValid = true - end - end - if not IsValid then - self:E({"File name invalid. Maybe something wrong with the extension ? ", FileName}) - end - - -- Check the Frequency - if type(Frequency) ~= "number" and IsValid then - self:E({"Frequency invalid. ", Frequency}) - IsValid = false - end - Frequency = Frequency * 1000000 -- Conversion to Hz - - -- Check the modulation - if Modulation ~= radio.modulation.AM and Modulation ~= radio.modulation.FM and IsValid then --TODO Maybe make this future proof if ED decides to add an other modulation ? - self:E({"Modulation is invalid. Use DCS's enum radio.modulation.", Modulation}) - IsValid = false - end - - -- Check the Power - if type(Power) ~= "number" and IsValid then - self:E({"Power is invalid. ", Power}) - IsValid = false - end - Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that - - if IsValid then - self:T2({"Activating Beacon on ", Frequency, Modulation}) - -- Note that this is looped. I have to give this transmission a unique name, I use the class ID - trigger.action.radioTransmission(FileName, self.Positionable:GetPositionVec3(), Modulation, true, Frequency, Power, tostring(self.ID)) - - if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD - SCHEDULER:New( nil, - function() - self:StopRadioBeacon() - end, {}, BeaconDuration) - end - end -end - ---- Stops the AA TACAN BEACON --- @param #BEACON self --- @return #BEACON self -function BEACON:StopRadioBeacon() - self:F() - -- The unique name of the transmission is the class ID - trigger.action.stopRadioTransmission(tostring(self.ID)) -end--- **Core** -- Spawn dynamically new STATICs in your missions. --- --- ![Banner Image](..\Presentations\SPAWNSTATIC\Dia1.JPG) --- --- ==== --- --- SPAWNSTATIC spawns static structures in your missions dynamically. See below the SPAWNSTATIC class documentation. --- --- ==== --- --- # Demo Missions --- --- ### [SPAWNSTATIC Demo Missions source code](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/SPS - Spawning Statics) --- --- ### [SPAWNSTATIC Demo Missions, only for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SPS%20-%20Spawning%20Statics) --- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) --- --- ==== --- --- # YouTube Channel --- --- ### [SPAWNSTATIC YouTube Channel]() --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- ### Contributions: --- --- ==== --- --- @module SpawnStatic - - - ---- @type SPAWNSTATIC --- @extends Core.Base#BASE - - ---- # SPAWNSTATIC class, extends @{Base#BASE} --- --- The SPAWNSTATIC class allows to spawn dynamically new @{Static}s. --- Through creating a copy of an existing static object template as defined in the Mission Editor (ME), --- SPAWNSTATIC can retireve the properties of the defined static object template (like type, category etc), and "copy" --- these properties to create a new static object and place it at the desired coordinate. --- --- New spawned @{Static}s get **the same name** as the name of the template Static, --- or gets the given name when a new name is provided at the Spawn method. --- By default, spawned @{Static}s will follow a naming convention at run-time: --- --- * Spawned @{Static}s will have the name _StaticName_#_nnn_, where _StaticName_ is the name of the **Template Static**, --- and _nnn_ is a **counter from 0 to 99999**. --- --- --- ## SPAWNSTATIC construction methods --- --- Create a new SPAWNSTATIC object with the @{#SPAWNSTATIC.NewFromStatic}(): --- --- * @{#SPAWNSTATIC.NewFromStatic}(): Creates a new SPAWNSTATIC object given a name that is used as the base of the naming of each spawned Static. --- --- ## **Spawn** methods --- --- Groups can be spawned at different times and methods: --- --- * @{#SPAWNSTATIC.SpawnFromPointVec2}(): Spawn a new group from a POINT_VEC2 coordinate. --- (The group will be spawned at land height ). --- * @{#SPAWNSTATIC.SpawnFromZone}(): Spawn a new group in a @{Zone}. --- --- @field #SPAWNSTATIC SPAWNSTATIC --- -SPAWNSTATIC = { - ClassName = "SPAWNSTATIC", -} - - ---- @type SPAWNSTATIC.SpawnZoneTable --- @list SpawnZone - - ---- Creates the main object to spawn a @{Static} defined in the ME. --- @param #SPAWNSTATIC self --- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template. Each new group will have the name starting with SpawnTemplatePrefix. --- @return #SPAWNSTATIC -function SPAWNSTATIC:NewFromStatic( SpawnTemplatePrefix, CountryID ) --R2.1 - local self = BASE:Inherit( self, BASE:New() ) -- #SPAWNSTATIC - self:F( { SpawnTemplatePrefix } ) - - local TemplateStatic = StaticObject.getByName( SpawnTemplatePrefix ) - if TemplateStatic then - self.SpawnTemplatePrefix = SpawnTemplatePrefix - self.CountryID = CountryID - self.SpawnIndex = 0 - else - error( "SPAWNSTATIC:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) - end - - self:SetEventPriority( 5 ) - - return self -end - ---- Creates the main object to spawn a @{Static} based on a type name. --- @param #SPAWNSTATIC self --- @param #string SpawnTypeName is the name of the type. --- @return #SPAWNSTATIC -function SPAWNSTATIC:NewFromType( SpawnTypeName, SpawnShapeName, SpawnCategory, CountryID ) --R2.1 - local self = BASE:Inherit( self, BASE:New() ) -- #SPAWNSTATIC - self:F( { SpawnTypeName } ) - - self.SpawnTypeName = SpawnTypeName - self.CountryID = CountryID - self.SpawnIndex = 0 - - self:SetEventPriority( 5 ) - - return self -end - - ---- Creates a new @{Static} from a POINT_VEC2. --- @param #SPAWNSTATIC self --- @param Core.Point#POINT_VEC2 PointVec2 The 2D coordinate where to spawn the static. --- @param #number Heading The heading of the static, which is a number in degrees from 0 to 360. --- @param #string (optional) The name of the new static. --- @return #SPAWNSTATIC -function SPAWNSTATIC:SpawnFromPointVec2( PointVec2, Heading, NewName ) --R2.1 - self:F( { PointVec2, Heading, NewName } ) - - local CountryName = _DATABASE.COUNTRY_NAME[self.CountryID] - - local StaticTemplate = _DATABASE:GetStaticUnitTemplate( self.SpawnTemplatePrefix ) - - StaticTemplate.x = PointVec2:GetLat() - StaticTemplate.y = PointVec2:GetLon() - - StaticTemplate.name = NewName or string.format("%s#%05d", self.SpawnTemplatePrefix, self.SpawnIndex ) - StaticTemplate.heading = ( Heading / 180 ) * math.pi - - StaticTemplate.CountryID = nil - StaticTemplate.CoalitionID = nil - StaticTemplate.CategoryID = nil - - local Static = coalition.addStaticObject( self.CountryID, StaticTemplate ) - - self.SpawnIndex = self.SpawnIndex + 1 - - return Static -end - ---- Creates a new @{Static} from a @{Zone}. --- @param #SPAWNSTATIC self --- @param Core.Zone#ZONE_BASE Zone The Zone where to spawn the static. --- @param #number Heading The heading of the static, which is a number in degrees from 0 to 360. --- @param #string (optional) The name of the new static. --- @return #SPAWNSTATIC -function SPAWNSTATIC:SpawnFromZone( Zone, Heading, NewName ) --R2.1 - self:F( { Zone, Heading, NewName } ) - - local Static = self:SpawnFromPointVec2( Zone:GetPointVec2(), Heading, NewName ) - - return Static -end - ---- **Core** -- Management of CARGO logistics, that can be transported from and to transportation carriers. --- --- ![Banner Image](..\Presentations\CARGO\Dia1.JPG) --- --- === --- --- Cargo can be of various forms, always are composed out of ONE object ( one unit or one static or one slingload crate ): --- --- * CARGO_UNIT, represented by a @{Unit} in a singleton @{Group}: Cargo can be represented by a Unit in a Group. a CARGO_UNIT is representable... --- * CARGO_GROUP, represented by a @{Group}. A CARGO_GROUP is reportable... --- --- This module is still under construction, but is described above works already, and will keep working ... --- --- ==== --- --- # Demo Missions --- --- ### [CARGO Demo Missions source code](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/CGO%20-%20Cargo) --- --- ### [CARGO Demo Missions, only for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/CGO%20-%20Cargo) --- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) --- --- ==== --- --- # YouTube Channel --- --- ### [CARGO YouTube Channel](https://www.youtube.com/watch?v=tM00lTlkpYs&list=PL7ZUrU4zZUl2zUTuKrLW5RsO9zLMqUtbf) --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- ### Contributions: --- --- ==== --- --- @module Cargo - --- Events - --- Board - ---- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#CARGO] Board --- @param #CARGO self --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. --- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). - ---- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#CARGO] __Board --- @param #CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. --- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). - - --- UnBoard - ---- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. --- The cargo must be in the **Loaded** state. --- @function [parent=#CARGO] UnBoard --- @param #CARGO self --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. - ---- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. --- The cargo must be in the **Loaded** state. --- @function [parent=#CARGO] __UnBoard --- @param #CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. - - --- Load - ---- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#CARGO] Load --- @param #CARGO self --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. - ---- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#CARGO] __Load --- @param #CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. - - --- UnLoad - ---- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. --- The cargo must be in the **Loaded** state. --- @function [parent=#CARGO] UnLoad --- @param #CARGO self --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. - ---- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. --- The cargo must be in the **Loaded** state. --- @function [parent=#CARGO] __UnLoad --- @param #CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. - --- State Transition Functions - --- UnLoaded - ---- @function [parent=#CARGO] OnLeaveUnLoaded --- @param #CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#CARGO] OnEnterUnLoaded --- @param #CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - --- Loaded - ---- @function [parent=#CARGO] OnLeaveLoaded --- @param #CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#CARGO] OnEnterLoaded --- @param #CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - --- Boarding - ---- @function [parent=#CARGO] OnLeaveBoarding --- @param #CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#CARGO] OnEnterBoarding --- @param #CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). - --- UnBoarding - ---- @function [parent=#CARGO] OnLeaveUnBoarding --- @param #CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#CARGO] OnEnterUnBoarding --- @param #CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - - --- TODO: Find all Carrier objects and make the type of the Carriers Wrapper.Unit#UNIT in the documentation. - -CARGOS = {} - -do -- CARGO - - --- @type CARGO - -- @extends Core.Fsm#FSM_PROCESS - -- @field #string Type A string defining the type of the cargo. eg. Engineers, Equipment, Screwdrivers. - -- @field #string Name A string defining the name of the cargo. The name is the unique identifier of the cargo. - -- @field #number Weight A number defining the weight of the cargo. The weight is expressed in kg. - -- @field #number NearRadius (optional) A number defining the radius in meters when the cargo is near to a Carrier, so that it can be loaded. - -- @field Wrapper.Controllable#CONTROLLABLE CargoObject The alive DCS object representing the cargo. This value can be nil, meaning, that the cargo is not represented anywhere... - -- @field Wrapper.Client#CLIENT CargoCarrier The alive DCS object carrying the cargo. This value can be nil, meaning, that the cargo is not contained anywhere... - -- @field #boolean Slingloadable This flag defines if the cargo can be slingloaded. - -- @field #boolean Moveable This flag defines if the cargo is moveable. - -- @field #boolean Representable This flag defines if the cargo can be represented by a DCS Unit. - -- @field #boolean Containable This flag defines if the cargo can be contained within a DCS Unit. - - --- # (R2.1) CARGO class, extends @{Fsm#FSM_PROCESS} - -- - -- The CARGO class defines the core functions that defines a cargo object within MOOSE. - -- A cargo is a logical object defined that is available for transport, and has a life status within a simulation. - -- - -- The CARGO is a state machine: it manages the different events and states of the cargo. - -- All derived classes from CARGO follow the same state machine, expose the same cargo event functions, and provide the same cargo states. - -- - -- ## CARGO Events: - -- - -- * @{#CARGO.Board}( ToCarrier ): Boards the cargo to a carrier. - -- * @{#CARGO.Load}( ToCarrier ): Loads the cargo into a carrier, regardless of its position. - -- * @{#CARGO.UnBoard}( ToPointVec2 ): UnBoard the cargo from a carrier. This will trigger a movement of the cargo to the option ToPointVec2. - -- * @{#CARGO.UnLoad}( ToPointVec2 ): UnLoads the cargo from a carrier. - -- * @{#CARGO.Dead}( Controllable ): The cargo is dead. The cargo process will be ended. - -- - -- ## CARGO States: - -- - -- * **UnLoaded**: The cargo is unloaded from a carrier. - -- * **Boarding**: The cargo is currently boarding (= running) into a carrier. - -- * **Loaded**: The cargo is loaded into a carrier. - -- * **UnBoarding**: The cargo is currently unboarding (=running) from a carrier. - -- * **Dead**: The cargo is dead ... - -- * **End**: The process has come to an end. - -- - -- ## CARGO state transition methods: - -- - -- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. - -- There are 2 moments when state transition methods will be called by the state machine: - -- - -- * **Leaving** the state. - -- The state transition method needs to start with the name **OnLeave + the name of the state**. - -- If the state transition method returns false, then the processing of the state transition will not be done! - -- If you want to change the behaviour of the AIControllable at this event, return false, - -- but then you'll need to specify your own logic using the AIControllable! - -- - -- * **Entering** the state. - -- The state transition method needs to start with the name **OnEnter + the name of the state**. - -- These state transition methods need to provide a return value, which is specified at the function description. - -- - -- @field #CARGO - CARGO = { - ClassName = "CARGO", - Type = nil, - Name = nil, - Weight = nil, - CargoObject = nil, - CargoCarrier = nil, - Representable = false, - Slingloadable = false, - Moveable = false, - Containable = false, - } - ---- @type CARGO.CargoObjects --- @map < #string, Wrapper.Positionable#POSITIONABLE > The alive POSITIONABLE objects representing the the cargo. - - ---- CARGO Constructor. This class is an abstract class and should not be instantiated. --- @param #CARGO self --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number NearRadius (optional) --- @return #CARGO -function CARGO:New( Type, Name, Weight ) --R2.1 - - local self = BASE:Inherit( self, FSM:New() ) -- #CARGO - self:F( { Type, Name, Weight } ) - - self:SetStartState( "UnLoaded" ) - self:AddTransition( { "UnLoaded", "Boarding" }, "Board", "Boarding" ) - self:AddTransition( "Boarding" , "Boarding", "Boarding" ) - self:AddTransition( "Boarding", "CancelBoarding", "UnLoaded" ) - self:AddTransition( "Boarding", "Load", "Loaded" ) - self:AddTransition( "UnLoaded", "Load", "Loaded" ) - self:AddTransition( "Loaded", "UnBoard", "UnBoarding" ) - self:AddTransition( "UnBoarding", "UnBoarding", "UnBoarding" ) - self:AddTransition( "UnBoarding", "UnLoad", "UnLoaded" ) - self:AddTransition( "Loaded", "UnLoad", "UnLoaded" ) - self:AddTransition( "*", "Damaged", "Damaged" ) - self:AddTransition( "*", "Destroyed", "Destroyed" ) - self:AddTransition( "*", "Respawn", "UnLoaded" ) - - - self.Type = Type - self.Name = Name - self.Weight = Weight - self.CargoObject = nil - self.CargoCarrier = nil - self.Representable = false - self.Slingloadable = false - self.Moveable = false - self.Containable = false - - self:SetDeployed( false ) - - self.CargoScheduler = SCHEDULER:New() - - CARGOS[self.Name] = self - - - return self -end - ---- Destroy the cargo. --- @param #CARGO self -function CARGO:Destroy() - if self.CargoObject then - self.CargoObject:Destroy() - end - self:Destroyed() -end - ---- Get the name of the Cargo. --- @param #CARGO self --- @return #string The name of the Cargo. -function CARGO:GetName() --R2.1 - return self.Name -end - ---- Get the object name of the Cargo. --- @param #CARGO self --- @return #string The object name of the Cargo. -function CARGO:GetObjectName() --R2.1 - if self:IsLoaded() then - return self.CargoCarrier:GetName() - else - return self.CargoObject:GetName() - end -end - ---- Get the type of the Cargo. --- @param #CARGO self --- @return #string The type of the Cargo. -function CARGO:GetType() - return self.Type -end - ---- Get the current coordinates of the Cargo. --- @param #CARGO self --- @return Core.Point#COORDINATE The coordinates of the Cargo. -function CARGO:GetCoordinate() - return self.CargoObject:GetCoordinate() -end - ---- Check if cargo is destroyed. --- @param #CARGO self --- @return #boolean true if destroyed -function CARGO:IsDestroyed() - return self:Is( "Destroyed" ) -end - - ---- Check if cargo is loaded. --- @param #CARGO self --- @return #boolean true if loaded -function CARGO:IsLoaded() - return self:Is( "Loaded" ) -end - ---- Check if cargo is unloaded. --- @param #CARGO self --- @return #boolean true if unloaded -function CARGO:IsUnLoaded() - return self:Is( "UnLoaded" ) -end - ---- Check if cargo is alive. --- @param #CARGO self --- @return #boolean true if unloaded -function CARGO:IsAlive() - - if self:IsLoaded() then - return self.CargoCarrier:IsAlive() - else - return self.CargoObject:IsAlive() - end -end - ---- Set the cargo as deployed --- @param #CARGO self -function CARGO:SetDeployed( Deployed ) - self.Deployed = Deployed -end - ---- Is the cargo deployed --- @param #CARGO self --- @return #boolean -function CARGO:IsDeployed() - return self.Deployed -end - - - - ---- Template method to spawn a new representation of the CARGO in the simulator. --- @param #CARGO self --- @return #CARGO -function CARGO:Spawn( PointVec2 ) - self:F() - -end - ---- Signal a flare at the position of the CARGO. --- @param #CARGO self --- @param Utilities.Utils#FLARECOLOR FlareColor -function CARGO:Flare( FlareColor ) - if self:IsUnLoaded() then - trigger.action.signalFlare( self.CargoObject:GetVec3(), FlareColor , 0 ) - end -end - ---- Signal a white flare at the position of the CARGO. --- @param #CARGO self -function CARGO:FlareWhite() - self:Flare( trigger.flareColor.White ) -end - ---- Signal a yellow flare at the position of the CARGO. --- @param #CARGO self -function CARGO:FlareYellow() - self:Flare( trigger.flareColor.Yellow ) -end - ---- Signal a green flare at the position of the CARGO. --- @param #CARGO self -function CARGO:FlareGreen() - self:Flare( trigger.flareColor.Green ) -end - ---- Signal a red flare at the position of the CARGO. --- @param #CARGO self -function CARGO:FlareRed() - self:Flare( trigger.flareColor.Red ) -end - ---- Smoke the CARGO. --- @param #CARGO self -function CARGO:Smoke( SmokeColor, Range ) - self:F2() - if self:IsUnLoaded() then - if Range then - trigger.action.smoke( self.CargoObject:GetRandomVec3( Range ), SmokeColor ) - else - trigger.action.smoke( self.CargoObject:GetVec3(), SmokeColor ) - end - end -end - ---- Smoke the CARGO Green. --- @param #CARGO self -function CARGO:SmokeGreen() - self:Smoke( trigger.smokeColor.Green, Range ) -end - ---- Smoke the CARGO Red. --- @param #CARGO self -function CARGO:SmokeRed() - self:Smoke( trigger.smokeColor.Red, Range ) -end - ---- Smoke the CARGO White. --- @param #CARGO self -function CARGO:SmokeWhite() - self:Smoke( trigger.smokeColor.White, Range ) -end - ---- Smoke the CARGO Orange. --- @param #CARGO self -function CARGO:SmokeOrange() - self:Smoke( trigger.smokeColor.Orange, Range ) -end - ---- Smoke the CARGO Blue. --- @param #CARGO self -function CARGO:SmokeBlue() - self:Smoke( trigger.smokeColor.Blue, Range ) -end - - - - - - ---- Check if Cargo is the given @{Zone}. --- @param #CARGO self --- @param Core.Zone#ZONE_BASE Zone --- @return #boolean **true** if cargo is in the Zone, **false** if cargo is not in the Zone. -function CARGO:IsInZone( Zone ) - self:F( { Zone } ) - - if self:IsLoaded() then - return Zone:IsPointVec2InZone( self.CargoCarrier:GetPointVec2() ) - else - self:F( { Size = self.CargoObject:GetSize(), Units = self.CargoObject:GetUnits() } ) - if self.CargoObject:GetSize() ~= 0 then - return Zone:IsPointVec2InZone( self.CargoObject:GetPointVec2() ) - else - return false - end - end - - return nil - -end - - ---- Check if CargoCarrier is near the Cargo to be Loaded. --- @param #CARGO self --- @param Core.Point#POINT_VEC2 PointVec2 --- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). --- @return #boolean -function CARGO:IsNear( PointVec2, NearRadius ) - self:F( { PointVec2, NearRadius } ) - - local Distance = PointVec2:DistanceFromPointVec2( self.CargoObject:GetPointVec2() ) - self:T( Distance ) - - if Distance <= NearRadius then - return true - else - return false - end -end - ---- Get the current PointVec2 of the cargo. --- @param #CARGO self --- @return Core.Point#POINT_VEC2 -function CARGO:GetPointVec2() - return self.CargoObject:GetPointVec2() -end - ---- Get the current Coordinate of the cargo. --- @param #CARGO self --- @return Core.Point#COORDINATE -function CARGO:GetCoordinate() - return self.CargoObject:GetCoordinate() -end - ---- Set the weight of the cargo. --- @param #CARGO self --- @param #number Weight The weight in kg. --- @return #CARGO -function CARGO:SetWeight( Weight ) - self.Weight = Weight - return self -end - -end - - -do -- CARGO_REPRESENTABLE - - --- @type CARGO_REPRESENTABLE - -- @extends #CARGO - -- @field test - - --- - -- @field #CARGO_REPRESENTABLE CARGO_REPRESENTABLE - CARGO_REPRESENTABLE = { - ClassName = "CARGO_REPRESENTABLE" - } - - --- CARGO_REPRESENTABLE Constructor. - -- @param #CARGO_REPRESENTABLE self - -- @param #string Type - -- @param #string Name - -- @param #number Weight - -- @param #number ReportRadius (optional) - -- @param #number NearRadius (optional) - -- @return #CARGO_REPRESENTABLE - function CARGO_REPRESENTABLE:New( CargoObject, Type, Name, Weight, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) ) -- #CARGO_REPRESENTABLE - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - return self - end - - --- Route a cargo unit to a PointVec2. - -- @param #CARGO_REPRESENTABLE self - -- @param Core.Point#POINT_VEC2 ToPointVec2 - -- @param #number Speed - -- @return #CARGO_REPRESENTABLE - function CARGO_REPRESENTABLE:RouteTo( ToPointVec2, Speed ) - self:F2( ToPointVec2 ) - - local Points = {} - - local PointStartVec2 = self.CargoObject:GetPointVec2() - - Points[#Points+1] = PointStartVec2:WaypointGround( Speed ) - Points[#Points+1] = ToPointVec2:WaypointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 2 ) - return self - end - - -end -- CARGO_REPRESENTABLE - - do -- CARGO_REPORTABLE - - --- @type CARGO_REPORTABLE - -- @extends #CARGO - CARGO_REPORTABLE = { - ClassName = "CARGO_REPORTABLE" - } - - --- CARGO_REPORTABLE Constructor. - -- @param #CARGO_REPORTABLE self - -- @param Wrapper.Controllable#Controllable CargoObject - -- @param #string Type - -- @param #string Name - -- @param #number Weight - -- @param #number ReportRadius (optional) - -- @param #number NearRadius (optional) - -- @return #CARGO_REPORTABLE - function CARGO_REPORTABLE:New( CargoObject, Type, Name, Weight, ReportRadius ) - local self = BASE:Inherit( self, CARGO:New( Type, Name, Weight ) ) -- #CARGO_REPORTABLE - self:F( { Type, Name, Weight, ReportRadius } ) - - self.CargoSet = SET_CARGO:New() -- Core.Set#SET_CARGO - - self.ReportRadius = ReportRadius or 1000 - self.CargoObject = CargoObject - - - - return self - end - - --- Check if CargoCarrier is in the ReportRadius for the Cargo to be Loaded. - -- @param #CARGO_REPORTABLE self - -- @param Core.Point#POINT_VEC2 PointVec2 - -- @return #boolean - function CARGO_REPORTABLE:IsInRadius( PointVec2 ) - self:F( { PointVec2 } ) - - local Distance = 0 - if self:IsLoaded() then - Distance = PointVec2:DistanceFromPointVec2( self.CargoCarrier:GetPointVec2() ) - else - Distance = PointVec2:DistanceFromPointVec2( self.CargoObject:GetPointVec2() ) - end - self:T( Distance ) - - if Distance <= self.ReportRadius then - return true - else - return false - end - - - end - - --- Send a CC message to a GROUP. - -- @param #CARGO_REPORTABLE self - -- @param #string Message - -- @param Wrapper.Group#GROUP TaskGroup - -- @param #sring Name (optional) The name of the Group used as a prefix for the message to the Group. If not provided, there will be nothing shown. - function CARGO_REPORTABLE:MessageToGroup( Message, TaskGroup, Name ) - - local Prefix = Name and "@ " .. Name .. ": " or "@ " .. TaskGroup:GetCallsign() .. ": " - Message = Prefix .. Message - MESSAGE:New( Message, 20, "Cargo: " .. self:GetName() ):ToGroup( TaskGroup ) - - end - - --- Get the range till cargo will board. - -- @param #CARGO_REPORTABLE self - -- @return #number The range till cargo will board. - function CARGO_REPORTABLE:GetBoardingRange() - return self.ReportRadius - end - - --- Respawn the cargo. - -- @param #CARGO_REPORTABLE self - function CARGO_REPORTABLE:Respawn() - - self:F({"Respawning"}) - - for CargoID, CargoData in pairs( self.CargoSet:GetSet() ) do - local Cargo = CargoData -- #CARGO - Cargo:Destroy() - Cargo:SetStartState( "UnLoaded" ) - end - - local CargoObject = self.CargoObject -- Wrapper.Group#GROUP - CargoObject:Destroy() - local Template = CargoObject:GetTemplate() - CargoObject:Respawn( Template ) - - self:SetDeployed( false ) - - local WeightGroup = 0 - - self:SetStartState( "UnLoaded" ) - - end - - -end - -do -- CARGO_UNIT - - --- Hello - -- @type CARGO_UNIT - -- @extends #CARGO_REPRESENTABLE - - --- # CARGO\_UNIT class, extends @{#CARGO_REPRESENTABLE} - -- - -- The CARGO\_UNIT class defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier. - -- Use the event functions as described above to Load, UnLoad, Board, UnBoard the CARGO\_UNIT objects to and from carriers. - -- - -- === - -- - -- @field #CARGO_UNIT CARGO_UNIT - -- - CARGO_UNIT = { - ClassName = "CARGO_UNIT" - } - ---- CARGO_UNIT Constructor. --- @param #CARGO_UNIT self --- @param Wrapper.Unit#UNIT CargoUnit --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #CARGO_UNIT -function CARGO_UNIT:New( CargoUnit, Type, Name, Weight, NearRadius ) - local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoUnit, Type, Name, Weight, NearRadius ) ) -- #CARGO_UNIT - self:F( { Type, Name, Weight, NearRadius } ) - - self:T( CargoUnit ) - self.CargoObject = CargoUnit - - self:T( self.ClassName ) - --- self:HandleEvent( EVENTS.Dead, --- --- @param #CARGO Cargo --- -- @param Core.Event#EVENTDATA EventData --- function( Cargo, EventData ) --- if Cargo:GetObjectName() == EventData.IniUnit:GetName() then --- self:E( { "Cargo destroyed", Cargo } ) --- Cargo:Destroyed() --- end --- end --- ) - - self:SetEventPriority( 5 ) - - return self -end - ---- CARGO_UNIT Destructor. --- @param #CARGO_UNIT self --- @return #CARGO_UNIT -function CARGO_UNIT:Destroy() - - -- Cargo objects are deleted from the _DATABASE and SET_CARGO objects. - self:F( { CargoName = self:GetName() } ) - _EVENTDISPATCHER:CreateEventDeleteCargo( self ) - - return self -end - ---- Enter UnBoarding State. --- @param #CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Point#POINT_VEC2 ToPointVec2 -function CARGO_UNIT:onenterUnBoarding( From, Event, To, ToPointVec2, NearRadius ) - self:F( { From, Event, To, ToPointVec2, NearRadius } ) - - NearRadius = NearRadius or 25 - - local Angle = 180 - local Speed = 60 - local DeployDistance = 9 - local RouteDistance = 60 - - if From == "Loaded" then - - local CargoCarrier = self.CargoCarrier -- Wrapper.Controllable#CONTROLLABLE - - local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - - - local CargoRoutePointVec2 = CargoCarrierPointVec2:Translate( RouteDistance, CargoDeployHeading ) - - - -- if there is no ToPointVec2 given, then use the CargoRoutePointVec2 - ToPointVec2 = ToPointVec2 or CargoRoutePointVec2 - local DirectionVec3 = CargoCarrierPointVec2:GetDirectionVec3(ToPointVec2) - local Angle = CargoCarrierPointVec2:GetAngleDegrees(DirectionVec3) - - local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( DeployDistance, Angle ) - - local FromPointVec2 = CargoCarrierPointVec2 - - -- Respawn the group... - if self.CargoObject then - self.CargoObject:ReSpawn( CargoDeployPointVec2:GetVec3(), CargoDeployHeading ) - self:F( { "CargoUnits:", self.CargoObject:GetGroup():GetName() } ) - self.CargoCarrier = nil - - local Points = {} - Points[#Points+1] = CargoCarrierPointVec2:WaypointGround( Speed ) - - Points[#Points+1] = ToPointVec2:WaypointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 1 ) - - - self:__UnBoarding( 1, ToPointVec2, NearRadius ) - end - end - -end - ---- Leave UnBoarding State. --- @param #CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Point#POINT_VEC2 ToPointVec2 -function CARGO_UNIT:onleaveUnBoarding( From, Event, To, ToPointVec2, NearRadius ) - self:F( { From, Event, To, ToPointVec2, NearRadius } ) - - NearRadius = NearRadius or 25 - - local Angle = 180 - local Speed = 10 - local Distance = 5 - - if From == "UnBoarding" then - if self:IsNear( ToPointVec2, NearRadius ) then - return true - else - - self:__UnBoarding( 1, ToPointVec2, NearRadius ) - end - return false - end - -end - ---- UnBoard Event. --- @param #CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Point#POINT_VEC2 ToPointVec2 -function CARGO_UNIT:onafterUnBoarding( From, Event, To, ToPointVec2, NearRadius ) - self:F( { From, Event, To, ToPointVec2, NearRadius } ) - - NearRadius = NearRadius or 25 - - self.CargoInAir = self.CargoObject:InAir() - - self:T( self.CargoInAir ) - - -- Only unboard the cargo when the carrier is not in the air. - -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). - if not self.CargoInAir then - - end - - self:__UnLoad( 1, ToPointVec2, NearRadius ) - -end - - - ---- Enter UnLoaded State. --- @param #CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Point#POINT_VEC2 -function CARGO_UNIT:onenterUnLoaded( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - local Angle = 180 - local Speed = 10 - local Distance = 5 - - if From == "Loaded" then - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = StartPointVec2:Translate( Distance, CargoDeployHeading ) - - ToPointVec2 = ToPointVec2 or POINT_VEC2:New( CargoDeployPointVec2:GetX(), CargoDeployPointVec2:GetY() ) - - -- Respawn the group... - if self.CargoObject then - self.CargoObject:ReSpawn( ToPointVec2:GetVec3(), 0 ) - self.CargoCarrier = nil - end - - end - - if self.OnUnLoadedCallBack then - self.OnUnLoadedCallBack( self, unpack( self.OnUnLoadedParameters ) ) - self.OnUnLoadedCallBack = nil - end - -end - ---- Board Event. --- @param #CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To -function CARGO_UNIT:onafterBoard( From, Event, To, CargoCarrier, NearRadius, ... ) - self:F( { From, Event, To, CargoCarrier, NearRadius } ) - - local NearRadius = NearRadius or 25 - - self.CargoInAir = self.CargoObject:InAir() - - self:T( self.CargoInAir ) - - -- Only move the group to the carrier when the cargo is not in the air - -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). - if not self.CargoInAir then - if self:IsNear( CargoCarrier:GetPointVec2(), NearRadius ) then - self:Load( CargoCarrier, NearRadius, ... ) - else - local Speed = 90 - local Angle = 180 - local Distance = 5 - - NearRadius = NearRadius or 25 - - local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2() - local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( Distance, CargoDeployHeading ) - - local Points = {} - - local PointStartVec2 = self.CargoObject:GetPointVec2() - - Points[#Points+1] = PointStartVec2:WaypointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 2 ) - self:__Boarding( -1, CargoCarrier, NearRadius ) - self.RunCount = 0 - end - end - -end - - ---- Boarding Event. --- @param #CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #number NearRadius -function CARGO_UNIT:onafterBoarding( From, Event, To, CargoCarrier, NearRadius, ... ) - self:F( { From, Event, To, CargoCarrier.UnitName, NearRadius } ) - - - if CargoCarrier and CargoCarrier:IsAlive() then - if CargoCarrier:InAir() == false then - if self:IsNear( CargoCarrier:GetPointVec2(), NearRadius ) then - self:__Load( 1, CargoCarrier, ... ) - else - self:__Boarding( -1, CargoCarrier, NearRadius, ... ) - self.RunCount = self.RunCount + 1 - if self.RunCount >= 20 then - self.RunCount = 0 - local Speed = 90 - local Angle = 180 - local Distance = 5 - - NearRadius = NearRadius or 25 - - local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2() - local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( Distance, CargoDeployHeading ) - - local Points = {} - - local PointStartVec2 = self.CargoObject:GetPointVec2() - - Points[#Points+1] = PointStartVec2:WaypointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 0.2 ) - end - end - else - self.CargoObject:MessageToGroup( "Cancelling Boarding... Get back on the ground!", 5, CargoCarrier:GetGroup(), self:GetName() ) - self:CancelBoarding( CargoCarrier, NearRadius, ... ) - self.CargoObject:SetCommand( self.CargoObject:CommandStopRoute( true ) ) - end - else - self:E("Something is wrong") - end - -end - - ---- Enter Boarding State. --- @param #CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function CARGO_UNIT:onenterBoarding( From, Event, To, CargoCarrier, NearRadius, ... ) - self:F( { From, Event, To, CargoCarrier.UnitName, NearRadius } ) - - local Speed = 90 - local Angle = 180 - local Distance = 5 - - local NearRadius = NearRadius or 25 - - if From == "UnLoaded" or From == "Boarding" then - - end - -end - ---- Loaded State. --- @param #CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function CARGO_UNIT:onenterLoaded( From, Event, To, CargoCarrier ) - self:F( { From, Event, To, CargoCarrier } ) - - self.CargoCarrier = CargoCarrier - - -- Only destroy the CargoObject is if there is a CargoObject (packages don't have CargoObjects). - if self.CargoObject then - self:T("Destroying") - self.CargoObject:Destroy() - end -end - - - -end - - -do -- CARGO_GROUP - - --- @type CARGO_GROUP - -- @extends #CARGO_REPORTABLE - - --- # CARGO\_GROUP class - -- - -- The CARGO\_GROUP class defines a cargo that is represented by a @{Group} object within the simulator, and can be transported by a carrier. - -- Use the event functions as described above to Load, UnLoad, Board, UnBoard the CARGO\_GROUP to and from carrier. - -- - -- @field #CARGO_GROUP CARGO_GROUP - -- - CARGO_GROUP = { - ClassName = "CARGO_GROUP", - } - ---- CARGO_GROUP constructor. --- @param #CARGO_GROUP self --- @param Wrapper.Group#GROUP CargoGroup --- @param #string Type --- @param #string Name --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #CARGO_GROUP -function CARGO_GROUP:New( CargoGroup, Type, Name, ReportRadius ) - local self = BASE:Inherit( self, CARGO_REPORTABLE:New( CargoGroup, Type, Name, 0, ReportRadius ) ) -- #CARGO_GROUP - self:F( { Type, Name, ReportRadius } ) - - self.CargoObject = CargoGroup - self:SetDeployed( false ) - self.CargoGroup = CargoGroup - - local WeightGroup = 0 - - for UnitID, UnitData in pairs( CargoGroup:GetUnits() ) do - local Unit = UnitData -- Wrapper.Unit#UNIT - local WeightUnit = Unit:GetDesc().massEmpty - WeightGroup = WeightGroup + WeightUnit - local CargoUnit = CARGO_UNIT:New( Unit, Type, Unit:GetName(), WeightUnit ) - self.CargoSet:Add( CargoUnit:GetName(), CargoUnit ) - end - - self:SetWeight( WeightGroup ) - - self:T( { "Weight Cargo", WeightGroup } ) - - -- Cargo objects are added to the _DATABASE and SET_CARGO objects. - _EVENTDISPATCHER:CreateEventNewCargo( self ) - - self:HandleEvent( EVENTS.Dead, self.OnEventCargoDead ) - self:HandleEvent( EVENTS.Crash, self.OnEventCargoDead ) - self:HandleEvent( EVENTS.PlayerLeaveUnit, self.OnEventCargoDead ) - - self:SetEventPriority( 4 ) - - return self -end - ---- @param #CARGO_GROUP self --- @param Core.Event#EVENTDATA EventData -function CARGO_GROUP:OnEventCargoDead( EventData ) - - local Destroyed = false - - if self:IsDestroyed() or self:IsUnLoaded() then - Destroyed = true - for CargoID, CargoData in pairs( self.CargoSet:GetSet() ) do - local Cargo = CargoData -- #CARGO - if Cargo:IsAlive() then - Destroyed = false - else - Cargo:Destroyed() - end - end - else - local CarrierName = self.CargoCarrier:GetName() - if CarrierName == EventData.IniDCSUnitName then - MESSAGE:New( "Cargo is lost from carrier " .. CarrierName, 15 ):ToAll() - Destroyed = true - self.CargoCarrier:ClearCargo() - end - end - - if Destroyed then - self:Destroyed() - self:E( { "Cargo group destroyed" } ) - end - -end - ---- Enter Boarding State. --- @param #CARGO_GROUP self --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #string Event --- @param #string From --- @param #string To -function CARGO_GROUP:onenterBoarding( From, Event, To, CargoCarrier, NearRadius, ... ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - local NearRadius = NearRadius or 25 - - if From == "UnLoaded" then - - -- For each Cargo object within the CARGO_GROUPED, route each object to the CargoLoadPointVec2 - self.CargoSet:ForEach( - function( Cargo, ... ) - Cargo:__Board( 1, CargoCarrier, NearRadius, ... ) - end, ... - ) - - self:__Boarding( 1, CargoCarrier, NearRadius, ... ) - end - -end - ---- Enter Loaded State. --- @param #CARGO_GROUP self --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #string Event --- @param #string From --- @param #string To -function CARGO_GROUP:onenterLoaded( From, Event, To, CargoCarrier, ... ) - self:F( { From, Event, To, CargoCarrier, ...} ) - - if From == "UnLoaded" then - -- For each Cargo object within the CARGO_GROUP, load each cargo to the CargoCarrier. - for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do - Cargo:Load( CargoCarrier ) - end - end - - --self.CargoObject:Destroy() - self.CargoCarrier = CargoCarrier - -end - ---- Leave Boarding State. --- @param #CARGO_GROUP self --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #string Event --- @param #string From --- @param #string To -function CARGO_GROUP:onafterBoarding( From, Event, To, CargoCarrier, NearRadius, ... ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - local NearRadius = NearRadius or 25 - - local Boarded = true - local Cancelled = false - local Dead = true - - self.CargoSet:Flush() - - -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 - for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do - self:T( { Cargo:GetName(), Cargo.current } ) - if not Cargo:is( "Loaded" ) then - Boarded = false - end - - if Cargo:is( "UnLoaded" ) then - Cancelled = true - end - - if not Cargo:is( "Destroyed" ) then - Dead = false - end - - end - - if not Dead then - - if not Cancelled then - if not Boarded then - self:__Boarding( 1, CargoCarrier, NearRadius, ... ) - else - self:__Load( 1, CargoCarrier, ... ) - end - else - self:__CancelBoarding( 1, CargoCarrier, NearRadius, ... ) - end - else - self:__Destroyed( 1, CargoCarrier, NearRadius, ... ) - end - -end - ---- Get the amount of cargo units in the group. --- @param #CARGO_GROUP self --- @return #CARGO_GROUP -function CARGO_GROUP:GetCount() - return self.CargoSet:Count() -end - - ---- Enter UnBoarding State. --- @param #CARGO_GROUP self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #string Event --- @param #string From --- @param #string To -function CARGO_GROUP:onenterUnBoarding( From, Event, To, ToPointVec2, NearRadius, ... ) - self:F( {From, Event, To, ToPointVec2, NearRadius } ) - - NearRadius = NearRadius or 25 - - local Timer = 1 - - if From == "Loaded" then - - if self.CargoObject then - self.CargoObject:Destroy() - end - - -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 - self.CargoSet:ForEach( - function( Cargo, NearRadius ) - - Cargo:__UnBoard( Timer, ToPointVec2, NearRadius ) - Timer = Timer + 10 - end, { NearRadius } - ) - - - self:__UnBoarding( 1, ToPointVec2, NearRadius, ... ) - end - -end - ---- Leave UnBoarding State. --- @param #CARGO_GROUP self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #string Event --- @param #string From --- @param #string To -function CARGO_GROUP:onleaveUnBoarding( From, Event, To, ToPointVec2, NearRadius, ... ) - self:F( { From, Event, To, ToPointVec2, NearRadius } ) - - --local NearRadius = NearRadius or 25 - - local Angle = 180 - local Speed = 10 - local Distance = 5 - - if From == "UnBoarding" then - local UnBoarded = true - - -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 - for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do - self:T( Cargo.current ) - if not Cargo:is( "UnLoaded" ) then - UnBoarded = false - end - end - - if UnBoarded then - return true - else - self:__UnBoarding( 1, ToPointVec2, NearRadius, ... ) - end - - return false - end - -end - ---- UnBoard Event. --- @param #CARGO_GROUP self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #string Event --- @param #string From --- @param #string To -function CARGO_GROUP:onafterUnBoarding( From, Event, To, ToPointVec2, NearRadius, ... ) - self:F( { From, Event, To, ToPointVec2, NearRadius } ) - - --local NearRadius = NearRadius or 25 - - self:__UnLoad( 1, ToPointVec2, ... ) -end - - - ---- Enter UnLoaded State. --- @param #CARGO_GROUP self --- @param Core.Point#POINT_VEC2 --- @param #string Event --- @param #string From --- @param #string To -function CARGO_GROUP:onenterUnLoaded( From, Event, To, ToPointVec2, ... ) - self:F( { From, Event, To, ToPointVec2 } ) - - if From == "Loaded" then - - -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 - self.CargoSet:ForEach( - function( Cargo ) - Cargo:UnLoad( ToPointVec2 ) - end - ) - - end - -end - - - --- Respawn the cargo when destroyed - -- @param #CARGO_GROUP self - -- @param #boolean RespawnDestroyed - function CARGO_GROUP:RespawnOnDestroyed( RespawnDestroyed ) - self:F({"In function RespawnOnDestroyed"}) - if RespawnDestroyed then - self.onenterDestroyed = function( self ) - self:F("IN FUNCTION") - self:Respawn() - end - else - self.onenterDestroyed = nil - end - - end - -end -- CARGO_GROUP - -do -- CARGO_PACKAGE - - --- @type CARGO_PACKAGE - -- @extends #CARGO_REPRESENTABLE - CARGO_PACKAGE = { - ClassName = "CARGO_PACKAGE" - } - ---- CARGO_PACKAGE Constructor. --- @param #CARGO_PACKAGE self --- @param Wrapper.Unit#UNIT CargoCarrier The UNIT carrying the package. --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #CARGO_PACKAGE -function CARGO_PACKAGE:New( CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius ) ) -- #CARGO_PACKAGE - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - self:T( CargoCarrier ) - self.CargoCarrier = CargoCarrier - - return self -end - ---- Board Event. --- @param #CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #number Speed --- @param #number BoardDistance --- @param #number Angle -function CARGO_PACKAGE:onafterOnBoard( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - self:F() - - self.CargoInAir = self.CargoCarrier:InAir() - - self:T( self.CargoInAir ) - - -- Only move the CargoCarrier to the New CargoCarrier when the New CargoCarrier is not in the air. - if not self.CargoInAir then - - local Points = {} - - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - self:T( { CargoCarrierHeading, CargoDeployHeading } ) - local CargoDeployPointVec2 = CargoCarrier:GetPointVec2():Translate( BoardDistance, CargoDeployHeading ) - - Points[#Points+1] = StartPointVec2:WaypointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) - - local TaskRoute = self.CargoCarrier:TaskRoute( Points ) - self.CargoCarrier:SetTask( TaskRoute, 1 ) - end - - self:Boarded( CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - -end - ---- Check if CargoCarrier is near the Cargo to be Loaded. --- @param #CARGO_PACKAGE self --- @param Wrapper.Unit#UNIT CargoCarrier --- @return #boolean -function CARGO_PACKAGE:IsNear( CargoCarrier ) - self:F() - - local CargoCarrierPoint = CargoCarrier:GetPointVec2() - - local Distance = CargoCarrierPoint:DistanceFromPointVec2( self.CargoCarrier:GetPointVec2() ) - self:T( Distance ) - - if Distance <= self.NearRadius then - return true - else - return false - end -end - ---- Boarded Event. --- @param #CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function CARGO_PACKAGE:onafterOnBoarded( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - self:F() - - if self:IsNear( CargoCarrier ) then - self:__Load( 1, CargoCarrier, Speed, LoadDistance, Angle ) - else - self:__Boarded( 1, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - end -end - ---- UnBoard Event. --- @param #CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param #number Speed --- @param #number UnLoadDistance --- @param #number UnBoardDistance --- @param #number Radius --- @param #number Angle -function CARGO_PACKAGE:onafterUnBoard( From, Event, To, CargoCarrier, Speed, UnLoadDistance, UnBoardDistance, Radius, Angle ) - self:F() - - self.CargoInAir = self.CargoCarrier:InAir() - - self:T( self.CargoInAir ) - - -- Only unboard the cargo when the carrier is not in the air. - -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). - if not self.CargoInAir then - - self:_Next( self.FsmP.UnLoad, UnLoadDistance, Angle ) - - local Points = {} - - local StartPointVec2 = CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - self:T( { CargoCarrierHeading, CargoDeployHeading } ) - local CargoDeployPointVec2 = StartPointVec2:Translate( UnBoardDistance, CargoDeployHeading ) - - Points[#Points+1] = StartPointVec2:WaypointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) - - local TaskRoute = CargoCarrier:TaskRoute( Points ) - CargoCarrier:SetTask( TaskRoute, 1 ) - end - - self:__UnBoarded( 1 , CargoCarrier, Speed ) - -end - ---- UnBoarded Event. --- @param #CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function CARGO_PACKAGE:onafterUnBoarded( From, Event, To, CargoCarrier, Speed ) - self:F() - - if self:IsNear( CargoCarrier ) then - self:__UnLoad( 1, CargoCarrier, Speed ) - else - self:__UnBoarded( 1, CargoCarrier, Speed ) - end -end - ---- Load Event. --- @param #CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #number Speed --- @param #number LoadDistance --- @param #number Angle -function CARGO_PACKAGE:onafterLoad( From, Event, To, CargoCarrier, Speed, LoadDistance, Angle ) - self:F() - - self.CargoCarrier = CargoCarrier - - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = StartPointVec2:Translate( LoadDistance, CargoDeployHeading ) - - local Points = {} - Points[#Points+1] = StartPointVec2:WaypointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) - - local TaskRoute = self.CargoCarrier:TaskRoute( Points ) - self.CargoCarrier:SetTask( TaskRoute, 1 ) - -end - ---- UnLoad Event. --- @param #CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param #number Distance --- @param #number Angle -function CARGO_PACKAGE:onafterUnLoad( From, Event, To, CargoCarrier, Speed, Distance, Angle ) - self:F() - - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = StartPointVec2:Translate( Distance, CargoDeployHeading ) - - self.CargoCarrier = CargoCarrier - - local Points = {} - Points[#Points+1] = StartPointVec2:WaypointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) - - local TaskRoute = self.CargoCarrier:TaskRoute( Points ) - self.CargoCarrier:SetTask( TaskRoute, 1 ) - -end - - -end ---- **Core** -- Management of SPOT logistics, that can be transported from and to transportation carriers. --- --- ![Banner Image](..\Presentations\SPOT\Dia1.JPG) --- --- ==== --- --- SPOT implements the DCS Spot class functionality, but adds additional luxury to be able to: --- --- * Spot for a defined duration. --- * wiggle the spot at the target. --- * Provide a @{Unit} as a target, instead of a point. --- * Implement a status machine, LaseOn, LaseOff. --- --- ==== --- --- # Demo Missions --- --- ### [SPOT Demo Missions source code]() --- --- ### [SPOT Demo Missions, only for beta testers]() --- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) --- --- ==== --- --- # YouTube Channel --- --- ### [SPOT YouTube Channel]() --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- ### Contributions: --- --- * [**Ciribob**](https://forums.eagle.ru/member.php?u=112175): Showing the way how to lase targets + how laser codes work!!! Explained the autolase script. --- * [**EasyEB**](https://forums.eagle.ru/member.php?u=112055): Ideas and Beta Testing --- * [**Wingthor**](https://forums.eagle.ru/member.php?u=123698): Beta Testing --- --- ==== --- --- @module Spot - - -do - - --- @type SPOT - -- @extends Core.Fsm#FSM - - - --- # SPOT class, extends @{Fsm#FSM} - -- - -- SPOT implements the DCS Spot class functionality, but adds additional luxury to be able to: - -- - -- * Mark targets for a defined duration. - -- * wiggle the spot at the target. - -- * Provide a @{Unit} as a target, instead of a point. - -- * Implement a status machine, LaseOn, LaseOff. - -- - -- ## 1. SPOT constructor - -- - -- * @{#SPOT.New}(..\Presentations\SPOT\Dia2.JPG): Creates a new SPOT object. - -- - -- ## 2. SPOT is a FSM - -- - -- ![Process]() - -- - -- ### 2.1 SPOT States - -- - -- * **Off**: Lasing is switched off. - -- * **On**: Lasing is switched on. - -- * **Destroyed**: Target is destroyed. - -- - -- ### 2.2 SPOT Events - -- - -- * **@{#SPOT.LaseOn}(Target, LaserCode, Duration)**: Lase to a target. - -- * **@{#SPOT.LaseOff}()**: Stop lasing the target. - -- * **@{#SPOT.Lasing}()**: Target is being lased. - -- * **@{#SPOT.Destroyed}()**: Triggered when target is destroyed. - -- - -- ## 3. Check if a Target is being lased - -- - -- The method @{#SPOT.IsLasing}() indicates whether lasing is on or off. - -- - -- @field #SPOT - SPOT = { - ClassName = "SPOT", - } - - --- SPOT Constructor. - -- @param #SPOT self - -- @param Wrapper.Unit#UNIT Recce - -- @param #number LaserCode - -- @param #number Duration - -- @return #SPOT - function SPOT:New( Recce ) - - local self = BASE:Inherit( self, FSM:New() ) -- #SPOT - self:F( {} ) - - self:SetStartState( "Off" ) - self:AddTransition( "Off", "LaseOn", "On" ) - - --- LaseOn Handler OnBefore for SPOT - -- @function [parent=#SPOT] OnBeforeLaseOn - -- @param #SPOT self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- LaseOn Handler OnAfter for SPOT - -- @function [parent=#SPOT] OnAfterLaseOn - -- @param #SPOT self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- LaseOn Trigger for SPOT - -- @function [parent=#SPOT] LaseOn - -- @param #SPOT self - - --- LaseOn Asynchronous Trigger for SPOT - -- @function [parent=#SPOT] __LaseOn - -- @param #SPOT self - -- @param #number Delay - - - - self:AddTransition( "On", "Lasing", "On" ) - self:AddTransition( { "On", "Destroyed" } , "LaseOff", "Off" ) - - --- LaseOff Handler OnBefore for SPOT - -- @function [parent=#SPOT] OnBeforeLaseOff - -- @param #SPOT self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- LaseOff Handler OnAfter for SPOT - -- @function [parent=#SPOT] OnAfterLaseOff - -- @param #SPOT self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- LaseOff Trigger for SPOT - -- @function [parent=#SPOT] LaseOff - -- @param #SPOT self - - --- LaseOff Asynchronous Trigger for SPOT - -- @function [parent=#SPOT] __LaseOff - -- @param #SPOT self - -- @param #number Delay - - self:AddTransition( "*" , "Destroyed", "Destroyed" ) - - --- Destroyed Handler OnBefore for SPOT - -- @function [parent=#SPOT] OnBeforeDestroyed - -- @param #SPOT self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- Destroyed Handler OnAfter for SPOT - -- @function [parent=#SPOT] OnAfterDestroyed - -- @param #SPOT self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- Destroyed Trigger for SPOT - -- @function [parent=#SPOT] Destroyed - -- @param #SPOT self - - --- Destroyed Asynchronous Trigger for SPOT - -- @function [parent=#SPOT] __Destroyed - -- @param #SPOT self - -- @param #number Delay - - - - self.Recce = Recce - - self.LaseScheduler = SCHEDULER:New( self ) - - self:SetEventPriority( 5 ) - - self.Lasing = false - - return self - end - - --- @param #SPOT self - -- @param From - -- @param Event - -- @param To - -- @param Wrapper.Positionable#POSITIONABLE Target - -- @param #number LaserCode - -- @param #number Duration - function SPOT:onafterLaseOn( From, Event, To, Target, LaserCode, Duration ) - self:E( { "LaseOn", Target, LaserCode, Duration } ) - - local function StopLase( self ) - self:LaseOff() - end - - self.Target = Target - self.LaserCode = LaserCode - - self.Lasing = true - - local RecceDcsUnit = self.Recce:GetDCSObject() - - self.SpotIR = Spot.createInfraRed( RecceDcsUnit, { x = 0, y = 2, z = 0 }, Target:GetPointVec3():AddY(1):GetVec3() ) - self.SpotLaser = Spot.createLaser( RecceDcsUnit, { x = 0, y = 2, z = 0 }, Target:GetPointVec3():AddY(1):GetVec3(), LaserCode ) - - if Duration then - self.ScheduleID = self.LaseScheduler:Schedule( self, StopLase, {self}, Duration ) - end - - self:HandleEvent( EVENTS.Dead ) - - self:__Lasing( -1 ) - end - - --- @param #SPOT self - -- @param Core.Event#EVENTDATA EventData - function SPOT:OnEventDead(EventData) - self:E( { Dead = EventData.IniDCSUnitName, Target = self.Target } ) - if self.Target then - if EventData.IniDCSUnitName == self.Target:GetName() then - self:E( {"Target dead ", self.Target:GetName() } ) - self:Destroyed() - self:LaseOff() - end - end - end - - --- @param #SPOT self - -- @param From - -- @param Event - -- @param To - function SPOT:onafterLasing( From, Event, To ) - - if self.Target:IsAlive() then - self.SpotIR:setPoint( self.Target:GetPointVec3():AddY(1):AddY(math.random(-100,100)/100):AddX(math.random(-100,100)/100):GetVec3() ) - self.SpotLaser:setPoint( self.Target:GetPointVec3():AddY(1):GetVec3() ) - self:__Lasing( -0.2 ) - else - self:E( { "Target is not alive", self.Target:IsAlive() } ) - end - - end - - --- @param #SPOT self - -- @param From - -- @param Event - -- @param To - -- @return #SPOT - function SPOT:onafterLaseOff( From, Event, To ) - - self:E( {"Stopped lasing for ", self.Target:GetName() , SpotIR = self.SportIR, SpotLaser = self.SpotLaser } ) - - self.Lasing = false - - self.SpotIR:destroy() - self.SpotLaser:destroy() - - self.SpotIR = nil - self.SpotLaser = nil - - if self.ScheduleID then - self.LaseScheduler:Stop(self.ScheduleID) - end - self.ScheduleID = nil - - self.Target = nil - - return self - end - - --- Check if the SPOT is lasing - -- @param #SPOT self - -- @return #boolean true if it is lasing - function SPOT:IsLasing() - return self.Lasing - end - -end--- **Wrapper** -- OBJECT wraps the DCS Object derived objects. --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- === --- --- @module Object - - ---- @type OBJECT --- @extends Core.Base#BASE --- @field #string ObjectName The name of the Object. - - ---- # OBJECT class, extends @{Base#BASE} --- --- OBJECT handles the DCS Object objects: --- --- * Support all DCS Object APIs. --- * Enhance with Object specific APIs not in the DCS Object API set. --- * Manage the "state" of the DCS Object. --- --- ## OBJECT constructor: --- --- The OBJECT class provides the following functions to construct a OBJECT instance: --- --- * @{Object#OBJECT.New}(): Create a OBJECT instance. --- --- @field #OBJECT -OBJECT = { - ClassName = "OBJECT", - ObjectName = "", -} - ---- A DCSObject --- @type DCSObject --- @field id_ The ID of the controllable in DCS - ---- Create a new OBJECT from a DCSObject --- @param #OBJECT self --- @param Dcs.DCSWrapper.Object#Object ObjectName The Object name --- @return #OBJECT self -function OBJECT:New( ObjectName, Test ) - local self = BASE:Inherit( self, BASE:New() ) - self:F2( ObjectName ) - self.ObjectName = ObjectName - - return self -end - - ---- Returns the unit's unique identifier. --- @param Wrapper.Object#OBJECT self --- @return Dcs.DCSWrapper.Object#Object.ID ObjectID --- @return #nil The DCS Object is not existing or alive. -function OBJECT:GetID() - self:F2( self.ObjectName ) - - local DCSObject = self:GetDCSObject() - - if DCSObject then - local ObjectID = DCSObject:getID() - return ObjectID - end - - return nil -end - ---- Destroys the OBJECT. --- @param #OBJECT self --- @return #nil The DCS Unit is not existing or alive. -function OBJECT:Destroy() - self:F2( self.ObjectName ) - - local DCSObject = self:GetDCSObject() - - if DCSObject then - - DCSObject:destroy() - end - - return nil -end - - - - ---- **Wrapper** -- IDENTIFIABLE is an intermediate class wrapping DCS Object class derived Objects. --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- ==== --- --- @module Identifiable - ---- @type IDENTIFIABLE --- @extends Wrapper.Object#OBJECT --- @field #string IdentifiableName The name of the identifiable. - ---- # IDENTIFIABLE class, extends @{Object#OBJECT} --- --- The IDENTIFIABLE class is a wrapper class to handle the DCS Identifiable objects: --- --- * Support all DCS Identifiable APIs. --- * Enhance with Identifiable specific APIs not in the DCS Identifiable API set. --- * Manage the "state" of the DCS Identifiable. --- --- ## IDENTIFIABLE constructor --- --- The IDENTIFIABLE class provides the following functions to construct a IDENTIFIABLE instance: --- --- * @{#IDENTIFIABLE.New}(): Create a IDENTIFIABLE instance. --- --- @field #IDENTIFIABLE -IDENTIFIABLE = { - ClassName = "IDENTIFIABLE", - IdentifiableName = "", -} - -local _CategoryName = { - [Unit.Category.AIRPLANE] = "Airplane", - [Unit.Category.HELICOPTER] = "Helicoper", - [Unit.Category.GROUND_UNIT] = "Ground Identifiable", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - } - ---- Create a new IDENTIFIABLE from a DCSIdentifiable --- @param #IDENTIFIABLE self --- @param Dcs.DCSWrapper.Identifiable#Identifiable IdentifiableName The DCS Identifiable name --- @return #IDENTIFIABLE self -function IDENTIFIABLE:New( IdentifiableName ) - local self = BASE:Inherit( self, OBJECT:New( IdentifiableName ) ) - self:F2( IdentifiableName ) - self.IdentifiableName = IdentifiableName - return self -end - ---- Returns if the Identifiable is alive. --- If the Identifiable is not alive, nil is returned. --- If the Identifiable is alive, true is returned. --- @param #IDENTIFIABLE self --- @return #boolean true if Identifiable is alive. --- @return #nil if the Identifiable is not existing or is not alive. -function IDENTIFIABLE:IsAlive() - self:F3( self.IdentifiableName ) - - local DCSIdentifiable = self:GetDCSObject() -- Dcs.DCSObject#Object - - if DCSIdentifiable then - local IdentifiableIsAlive = DCSIdentifiable:isExist() - return IdentifiableIsAlive - end - - return false -end - - - - ---- Returns DCS Identifiable object name. --- The function provides access to non-activated objects too. --- @param #IDENTIFIABLE self --- @return #string The name of the DCS Identifiable. --- @return #nil The DCS Identifiable is not existing or alive. -function IDENTIFIABLE:GetName() - self:F2( self.IdentifiableName ) - - local IdentifiableName = self.IdentifiableName - return IdentifiableName -end - - ---- Returns the type name of the DCS Identifiable. --- @param #IDENTIFIABLE self --- @return #string The type name of the DCS Identifiable. --- @return #nil The DCS Identifiable is not existing or alive. -function IDENTIFIABLE:GetTypeName() - self:F2( self.IdentifiableName ) - - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableTypeName = DCSIdentifiable:getTypeName() - self:T3( IdentifiableTypeName ) - return IdentifiableTypeName - end - - self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) - return nil -end - - ---- Returns category of the DCS Identifiable. --- @param #IDENTIFIABLE self --- @return Dcs.DCSWrapper.Object#Object.Category The category ID -function IDENTIFIABLE:GetCategory() - self:F2( self.ObjectName ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - local ObjectCategory = DCSObject:getCategory() - self:T3( ObjectCategory ) - return ObjectCategory - end - - return nil -end - - ---- Returns the DCS Identifiable category name as defined within the DCS Identifiable Descriptor. --- @param #IDENTIFIABLE self --- @return #string The DCS Identifiable Category Name -function IDENTIFIABLE:GetCategoryName() - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableCategoryName = _CategoryName[ self:GetDesc().category ] - return IdentifiableCategoryName - end - - self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) - return nil -end - ---- Returns coalition of the Identifiable. --- @param #IDENTIFIABLE self --- @return Dcs.DCSCoalitionWrapper.Object#coalition.side The side of the coalition. --- @return #nil The DCS Identifiable is not existing or alive. -function IDENTIFIABLE:GetCoalition() - self:F2( self.IdentifiableName ) - - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableCoalition = DCSIdentifiable:getCoalition() - self:T3( IdentifiableCoalition ) - return IdentifiableCoalition - end - - self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) - return nil -end - ---- Returns country of the Identifiable. --- @param #IDENTIFIABLE self --- @return Dcs.DCScountry#country.id The country identifier. --- @return #nil The DCS Identifiable is not existing or alive. -function IDENTIFIABLE:GetCountry() - self:F2( self.IdentifiableName ) - - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableCountry = DCSIdentifiable:getCountry() - self:T3( IdentifiableCountry ) - return IdentifiableCountry - end - - self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) - return nil -end - - - ---- Returns Identifiable descriptor. Descriptor type depends on Identifiable category. --- @param #IDENTIFIABLE self --- @return Dcs.DCSWrapper.Identifiable#Identifiable.Desc The Identifiable descriptor. --- @return #nil The DCS Identifiable is not existing or alive. -function IDENTIFIABLE:GetDesc() - self:F2( self.IdentifiableName ) - - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableDesc = DCSIdentifiable:getDesc() - self:T2( IdentifiableDesc ) - return IdentifiableDesc - end - - self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) - return nil -end - ---- Gets the CallSign of the IDENTIFIABLE, which is a blank by default. --- @param #IDENTIFIABLE self --- @return #string The CallSign of the IDENTIFIABLE. -function IDENTIFIABLE:GetCallsign() - return '' -end - - -function IDENTIFIABLE:GetThreatLevel() - - return 0, "Scenery" -end ---- **Wrapper** -- POSITIONABLE wraps DCS classes that are "positionable". --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- ==== --- --- @module Positionable - ---- @type POSITIONABLE.__ Methods which are not intended for mission designers, but which are used interally by the moose designer :-) --- @extends Wrapper.Identifiable#IDENTIFIABLE - ---- @type POSITIONABLE --- @extends Wrapper.Identifiable#IDENTIFIABLE - - ---- # POSITIONABLE class, extends @{Identifiable#IDENTIFIABLE} --- --- The POSITIONABLE class is a wrapper class to handle the POSITIONABLE objects: --- --- * Support all DCS APIs. --- * Enhance with POSITIONABLE specific APIs not in the DCS API set. --- * Manage the "state" of the POSITIONABLE. --- --- ## POSITIONABLE constructor --- --- The POSITIONABLE class provides the following functions to construct a POSITIONABLE instance: --- --- * @{#POSITIONABLE.New}(): Create a POSITIONABLE instance. --- --- ## Get the current speed --- --- There are 3 methods that can be used to determine the speed. --- Use @{#POSITIONABLE.GetVelocityKMH}() to retrieve the current speed in km/h. Use @{#POSITIONABLE.GetVelocityMPS}() to retrieve the speed in meters per second. --- The method @{#POSITIONABLE.GetVelocity}() returns the speed vector (a Vec3). --- --- ## Get the current altitude --- --- Altitude can be retrieved using the method @{#POSITIONABLE.GetHeight}() and returns the current altitude in meters from the orthonormal plane. --- --- --- @field #POSITIONABLE -POSITIONABLE = { - ClassName = "POSITIONABLE", - PositionableName = "", -} - ---- @field #POSITIONABLE.__ -POSITIONABLE.__ = {} - ---- @field #POSITIONABLE.__.Cargo -POSITIONABLE.__.Cargo = {} - - ---- A DCSPositionable --- @type DCSPositionable --- @field id_ The ID of the controllable in DCS - ---- Create a new POSITIONABLE from a DCSPositionable --- @param #POSITIONABLE self --- @param Dcs.DCSWrapper.Positionable#Positionable PositionableName The POSITIONABLE name --- @return #POSITIONABLE self -function POSITIONABLE:New( PositionableName ) - local self = BASE:Inherit( self, IDENTIFIABLE:New( PositionableName ) ) - - self.PositionableName = PositionableName - return self -end - ---- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Position The 3D position vectors of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetPositionVec3() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePosition = DCSPositionable:getPosition().p - self:T3( PositionablePosition ) - return PositionablePosition - end - - return nil -end - ---- Returns the @{DCSTypes#Vec2} vector indicating the point in 2D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec2 The 2D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetVec2() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = DCSPositionable:getPosition().p - - local PositionableVec2 = {} - PositionableVec2.x = PositionableVec3.x - PositionableVec2.y = PositionableVec3.z - - self:T2( PositionableVec2 ) - return PositionableVec2 - end - - return nil -end - ---- Returns a POINT_VEC2 object indicating the point in 2D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Core.Point#POINT_VEC2 The 2D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetPointVec2() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = DCSPositionable:getPosition().p - - local PositionablePointVec2 = POINT_VEC2:NewFromVec3( PositionableVec3 ) - - self:T2( PositionablePointVec2 ) - return PositionablePointVec2 - end - - return nil -end - ---- Returns a POINT_VEC3 object indicating the point in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Core.Point#POINT_VEC3 The 3D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetPointVec3() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = self:GetPositionVec3() - - local PositionablePointVec3 = POINT_VEC3:NewFromVec3( PositionableVec3 ) - - self:T2( PositionablePointVec3 ) - return PositionablePointVec3 - end - - return nil -end - ---- Returns a COORDINATE object indicating the point in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Core.Point#COORDINATE The COORDINATE of the POSITIONABLE. -function POSITIONABLE:GetCoordinate() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = self:GetPositionVec3() - - local PositionableCoordinate = COORDINATE:NewFromVec3( PositionableVec3 ) - PositionableCoordinate:SetHeading( self:GetHeading() ) - PositionableCoordinate:SetVelocity( self:GetVelocityMPS() ) - - self:T2( PositionableCoordinate ) - return PositionableCoordinate - end - - return nil -end - - ---- Returns a random @{DCSTypes#Vec3} vector within a range, indicating the point in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @param #number Radius --- @return Dcs.DCSTypes#Vec3 The 3D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. --- @usage --- -- If Radius is ignored, returns the Dcs.DCSTypes#Vec3 of first UNIT of the GROUP -function POSITIONABLE:GetRandomVec3( Radius ) - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePointVec3 = DCSPositionable:getPosition().p - - if Radius then - local PositionableRandomVec3 = {} - local angle = math.random() * math.pi*2; - PositionableRandomVec3.x = PositionablePointVec3.x + math.cos( angle ) * math.random() * Radius; - PositionableRandomVec3.y = PositionablePointVec3.y - PositionableRandomVec3.z = PositionablePointVec3.z + math.sin( angle ) * math.random() * Radius; - - self:T3( PositionableRandomVec3 ) - return PositionableRandomVec3 - else - self:E("Radius is nil, returning the PointVec3 of the POSITIONABLE", PositionablePointVec3) - return PositionablePointVec3 - end - end - - return nil -end - ---- Returns the @{DCSTypes#Vec3} vector indicating the 3D vector of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec3 The 3D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetVec3() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = DCSPositionable:getPosition().p - self:T3( PositionableVec3 ) - return PositionableVec3 - end - - return nil -end - - ---- Get the bounding box of the underlying POSITIONABLE DCS Object. --- @param #POSITIONABLE self --- @return Dcs.DCSTypes#Distance The bounding box of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetBoundingBox() --R2.1 - self:F2() - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableDesc = DCSPositionable:getDesc() --Dcs.DCSTypes#Desc - if PositionableDesc then - local PositionableBox = PositionableDesc.box - return PositionableBox - end - end - - return nil -end - - ---- Returns the altitude of the POSITIONABLE. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Distance The altitude of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetAltitude() - self:F2() - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePointVec3 = DCSPositionable:getPoint() --Dcs.DCSTypes#Vec3 - return PositionablePointVec3.y - end - - return nil -end - ---- Returns if the Positionable is located above a runway. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #boolean true if Positionable is above a runway. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:IsAboveRunway() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - - local Vec2 = self:GetVec2() - local SurfaceType = land.getSurfaceType( Vec2 ) - local IsAboveRunway = SurfaceType == land.SurfaceType.RUNWAY - - self:T2( IsAboveRunway ) - return IsAboveRunway - end - - return nil -end - - - ---- Returns the POSITIONABLE heading in degrees. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #number The POSTIONABLE heading --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetHeading() - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - - local PositionablePosition = DCSPositionable:getPosition() - if PositionablePosition then - local PositionableHeading = math.atan2( PositionablePosition.x.z, PositionablePosition.x.x ) - if PositionableHeading < 0 then - PositionableHeading = PositionableHeading + 2 * math.pi - end - PositionableHeading = PositionableHeading * 180 / math.pi - self:T2( PositionableHeading ) - return PositionableHeading - end - end - - return nil -end - - ---- Returns true if the POSITIONABLE is in the air. --- Polymorphic, is overridden in GROUP and UNIT. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #boolean true if in the air. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:InAir() - self:F2( self.PositionableName ) - - return nil -end - - ---- Returns the POSITIONABLE velocity vector. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec3 The velocity vector --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetVelocity() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVelocityVec3 = DCSPositionable:getVelocity() - self:T3( PositionableVelocityVec3 ) - return PositionableVelocityVec3 - end - - return nil -end - - ---- Returns the POSITIONABLE height in meters. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec3 The height of the positionable. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetHeight() --R2.1 - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePosition = DCSPositionable:getPosition() - if PositionablePosition then - local PositionableHeight = PositionablePosition.p.y - self:T2( PositionableHeight ) - return PositionableHeight - end - end - - return nil -end - - ---- Returns the POSITIONABLE velocity in km/h. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #number The velocity in km/h -function POSITIONABLE:GetVelocityKMH() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local VelocityVec3 = self:GetVelocity() - local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec - local Velocity = Velocity * 3.6 -- now it is in km/h. - self:T3( Velocity ) - return Velocity - end - - return 0 -end - ---- Returns the POSITIONABLE velocity in meters per second. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #number The velocity in meters per second. -function POSITIONABLE:GetVelocityMPS() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local VelocityVec3 = self:GetVelocity() - local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec - self:T3( Velocity ) - return Velocity - end - - return 0 -end - - ---- Returns the message text with the callsign embedded (if there is one). --- @param #POSITIONABLE self --- @param #string Message The message text --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. --- @return #string The message text -function POSITIONABLE:GetMessageText( Message, Name ) --R2.1 added - - local DCSObject = self:GetDCSObject() - if DCSObject then - Name = Name and ( " (" .. Name .. ")" ) or "" - local Callsign = string.format( "[%s]", self:GetCallsign() ~= "" and self:GetCallsign() or self:GetName() ) - local MessageText = Callsign .. Name .. ": " .. Message - return MessageText - end - - return nil -end - - ---- Returns a message with the callsign embedded (if there is one). --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. --- @return Core.Message#MESSAGE -function POSITIONABLE:GetMessage( Message, Duration, Name ) --R2.1 changed callsign and name and using GetMessageText - - local DCSObject = self:GetDCSObject() - if DCSObject then - local MessageText = self:GetMessageText( Message, Name ) - return MESSAGE:New( MessageText, Duration ) - end - - return nil -end - ---- Returns a message of a specified type with the callsign embedded (if there is one). --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Core.Message#MESSAGE MessageType MessageType The message type. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. --- @return Core.Message#MESSAGE -function POSITIONABLE:GetMessageType( Message, MessageType, Name ) -- R2.2 changed callsign and name and using GetMessageText - - local DCSObject = self:GetDCSObject() - if DCSObject then - local MessageText = self:GetMessageText( Message, Name ) - return MESSAGE:NewType( MessageText, MessageType ) - end - - return nil -end - ---- Send a message to all coalitions. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToAll( Message, Duration, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToAll() - end - - return nil -end - ---- Send a message to a coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTYpes#Duration Duration The duration of the message. --- @param Dcs.DCScoalition#coalition MessageCoalition The Coalition receiving the message. -function POSITIONABLE:MessageToCoalition( Message, Duration, MessageCoalition ) - self:F2( { Message, Duration } ) - - local Name = "" - - local DCSObject = self:GetDCSObject() - if DCSObject then - if MessageCoalition == coalition.side.BLUE then - Name = "Blue coalition" - end - if MessageCoalition == coalition.side.RED then - Name = "Red coalition" - end - self:GetMessage( Message, Duration, Name ):ToCoalition( MessageCoalition ) - end - - return nil -end - - ---- Send a message to a coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Core.Message#MESSAGE.Type MessageType The message type that determines the duration. --- @param Dcs.DCScoalition#coalition MessageCoalition The Coalition receiving the message. -function POSITIONABLE:MessageTypeToCoalition( Message, MessageType, MessageCoalition ) - self:F2( { Message, MessageType } ) - - local Name = "" - - local DCSObject = self:GetDCSObject() - if DCSObject then - if MessageCoalition == coalition.side.BLUE then - Name = "Blue coalition" - end - if MessageCoalition == coalition.side.RED then - Name = "Red coalition" - end - self:GetMessageType( Message, MessageType, Name ):ToCoalition( MessageCoalition ) - end - - return nil -end - - ---- Send a message to the red coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTYpes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToRed( Message, Duration, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToRed() - end - - return nil -end - ---- Send a message to the blue coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToBlue( Message, Duration, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToBlue() - end - - return nil -end - ---- Send a message to a client. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param Wrapper.Client#CLIENT Client The client object receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToClient( Message, Duration, Client, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToClient( Client ) - end - - return nil -end - ---- Send a message to a @{Group}. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param Wrapper.Group#GROUP MessageGroup The GROUP object receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToGroup( Message, Duration, MessageGroup, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - if DCSObject:isExist() then - self:GetMessage( Message, Duration, Name ):ToGroup( MessageGroup ) - end - end - - return nil -end - ---- Send a message of a message type to a @{Group}. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Core.Message#MESSAGE.Type MessageType The message type that determines the duration. --- @param Wrapper.Group#GROUP MessageGroup The GROUP object receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageTypeToGroup( Message, MessageType, MessageGroup, Name ) - self:F2( { Message, MessageType } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - if DCSObject:isExist() then - self:GetMessageType( Message, MessageType, Name ):ToGroup( MessageGroup ) - end - end - - return nil -end - ---- Send a message to a @{Set#SET_GROUP}. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param Core.Set#SET_GROUP MessageSetGroup The SET_GROUP collection receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToSetGroup( Message, Duration, MessageSetGroup, Name ) --R2.1 - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - if DCSObject:isExist() then - MessageSetGroup:ForEachGroup( - function( MessageGroup ) - self:GetMessage( Message, Duration, Name ):ToGroup( MessageGroup ) - end - ) - end - end - - return nil -end - ---- Send a message to the players in the @{Group}. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:Message( Message, Duration, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToGroup( self ) - end - - return nil -end - ---- Create a @{Radio#RADIO}, to allow radio transmission for this POSITIONABLE. --- Set parameters with the methods provided, then use RADIO:Broadcast() to actually broadcast the message --- @param #POSITIONABLE self --- @return #RADIO Radio -function POSITIONABLE:GetRadio() --R2.1 - self:F2(self) - return RADIO:New(self) -end - ---- Create a @{Radio#BEACON}, to allow this POSITIONABLE to broadcast beacon signals --- @param #POSITIONABLE self --- @return #RADIO Radio -function POSITIONABLE:GetBeacon() --R2.1 - self:F2(self) - return BEACON:New(self) -end - ---- Start Lasing a POSITIONABLE --- @param #POSITIONABLE self --- @param #POSITIONABLE Target --- @param #number LaserCode --- @param #number Duration --- @return Core.Spot#SPOT -function POSITIONABLE:LaseUnit( Target, LaserCode, Duration ) --R2.1 - self:F2() - - LaserCode = LaserCode or math.random( 1000, 9999 ) - - local RecceDcsUnit = self:GetDCSObject() - local TargetVec3 = Target:GetVec3() - - self:E("bulding spot") - self.Spot = SPOT:New( self ) -- Core.Spot#SPOT - self.Spot:LaseOn( Target, LaserCode, Duration) - self.LaserCode = LaserCode - - return self.Spot - -end - ---- Stop Lasing a POSITIONABLE --- @param #POSITIONABLE self --- @return #POSITIONABLE -function POSITIONABLE:LaseOff() --R2.1 - self:F2() - - if self.Spot then - self.Spot:LaseOff() - self.Spot = nil - end - - return self -end - ---- Check if the POSITIONABLE is lasing a target --- @param #POSITIONABLE self --- @return #boolean true if it is lasing a target -function POSITIONABLE:IsLasing() --R2.1 - self:F2() - - local Lasing = false - - if self.Spot then - Lasing = self.Spot:IsLasing() - end - - return Lasing -end - ---- Get the Spot --- @param #POSITIONABLE self --- @return Core.Spot#SPOT The Spot -function POSITIONABLE:GetSpot() --R2.1 - - return self.Spot -end - ---- Get the last assigned laser code --- @param #POSITIONABLE self --- @return #number The laser code -function POSITIONABLE:GetLaserCode() --R2.1 - - return self.LaserCode -end - ---- Add cargo. --- @param #POSITIONABLE self --- @param Core.Cargo#CARGO Cargo --- @return #POSITIONABLE -function POSITIONABLE:AddCargo( Cargo ) - self.__.Cargo[Cargo] = Cargo - return self -end - ---- Remove cargo. --- @param #POSITIONABLE self --- @param Core.Cargo#CARGO Cargo --- @return #POSITIONABLE -function POSITIONABLE:RemoveCargo( Cargo ) - self.__.Cargo[Cargo] = nil - return self -end - ---- Returns if carrier has given cargo. --- @param #POSITIONABLE self --- @return Core.Cargo#CARGO Cargo -function POSITIONABLE:HasCargo( Cargo ) - return self.__.Cargo[Cargo] -end - ---- Clear all cargo. --- @param #POSITIONABLE self -function POSITIONABLE:ClearCargo() - self.__.Cargo = {} -end - ---- Get cargo item count. --- @param #POSITIONABLE self --- @return Core.Cargo#CARGO Cargo -function POSITIONABLE:CargoItemCount() - local ItemCount = 0 - for CargoName, Cargo in pairs( self.__.Cargo ) do - ItemCount = ItemCount + Cargo:GetCount() - end - return ItemCount -end - ---- Signal a flare at the position of the POSITIONABLE. --- @param #POSITIONABLE self --- @param Utilities.Utils#FLARECOLOR FlareColor -function POSITIONABLE:Flare( FlareColor ) - self:F2() - trigger.action.signalFlare( self:GetVec3(), FlareColor , 0 ) -end - ---- Signal a white flare at the position of the POSITIONABLE. --- @param #POSITIONABLE self -function POSITIONABLE:FlareWhite() - self:F2() - trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.White , 0 ) -end - ---- Signal a yellow flare at the position of the POSITIONABLE. --- @param #POSITIONABLE self -function POSITIONABLE:FlareYellow() - self:F2() - trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Yellow , 0 ) -end - ---- Signal a green flare at the position of the POSITIONABLE. --- @param #POSITIONABLE self -function POSITIONABLE:FlareGreen() - self:F2() - trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Green , 0 ) -end - ---- Signal a red flare at the position of the POSITIONABLE. --- @param #POSITIONABLE self -function POSITIONABLE:FlareRed() - self:F2() - local Vec3 = self:GetVec3() - if Vec3 then - trigger.action.signalFlare( Vec3, trigger.flareColor.Red, 0 ) - end -end - ---- Smoke the POSITIONABLE. --- @param #POSITIONABLE self --- @param Utilities.Utils#SMOKECOLOR SmokeColor The color to smoke to positionable. --- @param #number Range The range in meters to randomize the smoking around the positionable. --- @param #number AddHeight The height in meters to add to the altitude of the positionable. -function POSITIONABLE:Smoke( SmokeColor, Range, AddHeight ) - self:F2() - if Range then - local Vec3 = self:GetRandomVec3( Range ) - Vec3.y = Vec3.y + AddHeight or 0 - trigger.action.smoke( Vec3, SmokeColor ) - else - local Vec3 = self:GetVec3() - Vec3.y = Vec3.y + AddHeight or 0 - trigger.action.smoke( self:GetVec3(), SmokeColor ) - end - -end - ---- Smoke the POSITIONABLE Green. --- @param #POSITIONABLE self -function POSITIONABLE:SmokeGreen() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Green ) -end - ---- Smoke the POSITIONABLE Red. --- @param #POSITIONABLE self -function POSITIONABLE:SmokeRed() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Red ) -end - ---- Smoke the POSITIONABLE White. --- @param #POSITIONABLE self -function POSITIONABLE:SmokeWhite() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.White ) -end - ---- Smoke the POSITIONABLE Orange. --- @param #POSITIONABLE self -function POSITIONABLE:SmokeOrange() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Orange ) -end - ---- Smoke the POSITIONABLE Blue. --- @param #POSITIONABLE self -function POSITIONABLE:SmokeBlue() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Blue ) -end - - ---- **Wrapper** -- CONTROLLABLE is an intermediate class wrapping Group and Unit classes "controllers". --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- === --- --- @module Controllable - - - ---- @type CONTROLLABLE --- @extends Wrapper.Positionable#POSITIONABLE --- @field Dcs.DCSWrapper.Controllable#Controllable DCSControllable The DCS controllable class. --- @field #string ControllableName The name of the controllable. - - - ---- # CONTROLLABLE class, extends @{Positionable#POSITIONABLE} --- --- CONTROLLABLE is a wrapper class to handle the "DCS Controllable objects", which are Groups and Units: --- --- * Support all DCS Controllable APIs. --- * Enhance with Controllable specific APIs not in the DCS Controllable API set. --- * Handle local Controllable Controller. --- * Manage the "state" of the DCS Controllable. --- --- ## CONTROLLABLE constructor --- --- The CONTROLLABLE class provides the following functions to construct a CONTROLLABLE instance: --- --- * @{#CONTROLLABLE.New}(): Create a CONTROLLABLE instance. --- --- ## CONTROLLABLE Task methods --- --- Several controllable task methods are available that help you to prepare tasks. --- These methods return a string consisting of the task description, which can then be given to either a @{Controllable#CONTROLLABLE.PushTask} or @{Controllable#SetTask} method to assign the task to the CONTROLLABLE. --- Tasks are specific for the category of the CONTROLLABLE, more specific, for AIR, GROUND or AIR and GROUND. --- Each task description where applicable indicates for which controllable category the task is valid. --- There are 2 main subdivisions of tasks: Assigned tasks and EnRoute tasks. --- --- ### Task assignment --- --- Assigned task methods make the controllable execute the task where the location of the (possible) targets of the task are known before being detected. --- This is different from the EnRoute tasks, where the targets of the task need to be detected before the task can be executed. --- --- Find below a list of the **assigned task** methods: --- --- * @{#CONTROLLABLE.TaskAttackGroup}: (AIR) Attack a Controllable. --- * @{#CONTROLLABLE.TaskAttackMapObject}: (AIR) Attacking the map object (building, structure, e.t.c). --- * @{#CONTROLLABLE.TaskAttackUnit}: (AIR) Attack the Unit. --- * @{#CONTROLLABLE.TaskBombing}: (AIR) Delivering weapon at the point on the ground. --- * @{#CONTROLLABLE.TaskBombingRunway}: (AIR) Delivering weapon on the runway. --- * @{#CONTROLLABLE.TaskEmbarking}: (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. --- * @{#CONTROLLABLE.TaskEmbarkToTransport}: (GROUND) Embark to a Transport landed at a location. --- * @{#CONTROLLABLE.TaskEscort}: (AIR) Escort another airborne controllable. --- * @{#CONTROLLABLE.TaskFAC_AttackGroup}: (AIR + GROUND) The task makes the controllable/unit a FAC and orders the FAC to control the target (enemy ground controllable) destruction. --- * @{#CONTROLLABLE.TaskFireAtPoint}: (GROUND) Fire some or all ammunition at a VEC2 point. --- * @{#CONTROLLABLE.TaskFollow}: (AIR) Following another airborne controllable. --- * @{#CONTROLLABLE.TaskHold}: (GROUND) Hold ground controllable from moving. --- * @{#CONTROLLABLE.TaskHoldPosition}: (AIR) Hold position at the current position of the first unit of the controllable. --- * @{#CONTROLLABLE.TaskLand}: (AIR HELICOPTER) Landing at the ground. For helicopters only. --- * @{#CONTROLLABLE.TaskLandAtZone}: (AIR) Land the controllable at a @{Zone#ZONE_RADIUS). --- * @{#CONTROLLABLE.TaskOrbitCircle}: (AIR) Orbit at the current position of the first unit of the controllable at a specified alititude. --- * @{#CONTROLLABLE.TaskOrbitCircleAtVec2}: (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. --- * @{#CONTROLLABLE.TaskRefueling}: (AIR) Refueling from the nearest tanker. No parameters. --- * @{#CONTROLLABLE.TaskRoute}: (AIR + GROUND) Return a Misson task to follow a given route defined by Points. --- * @{#CONTROLLABLE.TaskRouteToVec2}: (AIR + GROUND) Make the Controllable move to a given point. --- * @{#CONTROLLABLE.TaskRouteToVec3}: (AIR + GROUND) Make the Controllable move to a given point. --- * @{#CONTROLLABLE.TaskRouteToZone}: (AIR + GROUND) Route the controllable to a given zone. --- * @{#CONTROLLABLE.TaskReturnToBase}: (AIR) Route the controllable to an airbase. --- --- ### EnRoute assignment --- --- EnRoute tasks require the targets of the task need to be detected by the controllable (using its sensors) before the task can be executed: --- --- * @{#CONTROLLABLE.EnRouteTaskAWACS}: (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters. --- * @{#CONTROLLABLE.EnRouteTaskEngageControllable}: (AIR) Engaging a controllable. The task does not assign the target controllable to the unit/controllable to attack now; it just allows the unit/controllable to engage the target controllable as well as other assigned targets. --- * @{#CONTROLLABLE.EnRouteTaskEngageTargets}: (AIR) Engaging targets of defined types. --- * @{#CONTROLLABLE.EnRouteTaskEngageTargetsInZone}: (AIR) Engaging a targets of defined types at circle-shaped zone. --- * @{#CONTROLLABLE.EnRouteTaskEWR}: (AIR) Attack the Unit. --- * @{#CONTROLLABLE.EnRouteTaskFAC}: (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose a targets (enemy ground controllable) around as well as other assigned targets. --- * @{#CONTROLLABLE.EnRouteTaskFAC_EngageControllable}: (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose the target (enemy ground controllable) as well as other assigned targets. --- * @{#CONTROLLABLE.EnRouteTaskTanker}: (AIR) Aircraft will act as a tanker for friendly units. No parameters. --- --- ### Task preparation --- --- There are certain task methods that allow to tailor the task behaviour: --- --- * @{#CONTROLLABLE.TaskWrappedAction}: Return a WrappedAction Task taking a Command. --- * @{#CONTROLLABLE.TaskCombo}: Return a Combo Task taking an array of Tasks. --- * @{#CONTROLLABLE.TaskCondition}: Return a condition section for a controlled task. --- * @{#CONTROLLABLE.TaskControlled}: Return a Controlled Task taking a Task and a TaskCondition. --- --- ### Call a function as a Task --- --- A function can be called which is part of a Task. The method @{#CONTROLLABLE.TaskFunction}() prepares --- a Task that can call a GLOBAL function from within the Controller execution. --- This method can also be used to **embed a function call when a certain waypoint has been reached**. --- See below the **Tasks at Waypoints** section. --- --- Demonstration Mission: [GRP-502 - Route at waypoint to random point](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/GRP - Group Commands/GRP-502 - Route at waypoint to random point) --- --- ### Tasks at Waypoints --- --- Special Task methods are available to set tasks at certain waypoints. --- The method @{#CONTROLLABLE.SetTaskWaypoint}() helps preparing a Route, embedding a Task at the Waypoint of the Route. --- --- This creates a Task element, with an action to call a function as part of a Wrapped Task. --- --- ### Obtain the mission from controllable templates --- --- Controllable templates contain complete mission descriptions. Sometimes you want to copy a complete mission from a controllable and assign it to another: --- --- * @{#CONTROLLABLE.TaskMission}: (AIR + GROUND) Return a mission task from a mission template. --- --- ## CONTROLLABLE Command methods --- --- Controllable **command methods** prepare the execution of commands using the @{#CONTROLLABLE.SetCommand} method: --- --- * @{#CONTROLLABLE.CommandDoScript}: Do Script command. --- * @{#CONTROLLABLE.CommandSwitchWayPoint}: Perform a switch waypoint command. --- --- ## Routing of Controllables --- --- Different routing methods exist to route GROUPs and UNITs to different locations: --- --- * @{#CONTROLLABLE.Route}(): Make the Controllable to follow a given route. --- * @{#CONTROLLABLE.RouteGroundTo}(): Make the GROUND Controllable to drive towards a specific coordinate. --- * @{#CONTROLLABLE.RouteAirTo}(): Make the AIR Controllable to fly towards a specific coordinate. --- --- ## Option methods --- --- Controllable **Option methods** change the behaviour of the Controllable while being alive. --- --- ### Rule of Engagement: --- --- * @{#CONTROLLABLE.OptionROEWeaponFree} --- * @{#CONTROLLABLE.OptionROEOpenFire} --- * @{#CONTROLLABLE.OptionROEReturnFire} --- * @{#CONTROLLABLE.OptionROEEvadeFire} --- --- To check whether an ROE option is valid for a specific controllable, use: --- --- * @{#CONTROLLABLE.OptionROEWeaponFreePossible} --- * @{#CONTROLLABLE.OptionROEOpenFirePossible} --- * @{#CONTROLLABLE.OptionROEReturnFirePossible} --- * @{#CONTROLLABLE.OptionROEEvadeFirePossible} --- --- ### Rule on thread: --- --- * @{#CONTROLLABLE.OptionROTNoReaction} --- * @{#CONTROLLABLE.OptionROTPassiveDefense} --- * @{#CONTROLLABLE.OptionROTEvadeFire} --- * @{#CONTROLLABLE.OptionROTVertical} --- --- To test whether an ROT option is valid for a specific controllable, use: --- --- * @{#CONTROLLABLE.OptionROTNoReactionPossible} --- * @{#CONTROLLABLE.OptionROTPassiveDefensePossible} --- * @{#CONTROLLABLE.OptionROTEvadeFirePossible} --- * @{#CONTROLLABLE.OptionROTVerticalPossible} --- --- @field #CONTROLLABLE -CONTROLLABLE = { - ClassName = "CONTROLLABLE", - ControllableName = "", - WayPointFunctions = {}, -} - ---- Create a new CONTROLLABLE from a DCSControllable --- @param #CONTROLLABLE self --- @param Dcs.DCSWrapper.Controllable#Controllable ControllableName The DCS Controllable name --- @return #CONTROLLABLE self -function CONTROLLABLE:New( ControllableName ) - local self = BASE:Inherit( self, POSITIONABLE:New( ControllableName ) ) -- #CONTROLLABLE - self:F2( ControllableName ) - self.ControllableName = ControllableName - - self.TaskScheduler = SCHEDULER:New( self ) - return self -end - --- DCS Controllable methods support. - ---- Get the controller for the CONTROLLABLE. --- @param #CONTROLLABLE self --- @return Dcs.DCSController#Controller -function CONTROLLABLE:_GetController() - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local ControllableController = DCSControllable:getController() - return ControllableController - end - - return nil -end - --- Get methods - ---- Returns the UNITs wrappers of the DCS Units of the Controllable (default is a GROUP). --- @param #CONTROLLABLE self --- @return #list The UNITs wrappers. -function CONTROLLABLE:GetUnits() - self:F2( { self.ControllableName } ) - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local DCSUnits = DCSControllable:getUnits() - local Units = {} - for Index, UnitData in pairs( DCSUnits ) do - Units[#Units+1] = UNIT:Find( UnitData ) - end - self:T3( Units ) - return Units - end - - return nil -end - - ---- Returns the health. Dead controllables have health <= 1.0. --- @param #CONTROLLABLE self --- @return #number The controllable health value (unit or group average). --- @return #nil The controllable is not existing or alive. -function CONTROLLABLE:GetLife() - self:F2( self.ControllableName ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local UnitLife = 0 - local Units = self:GetUnits() - if #Units == 1 then - local Unit = Units[1] -- Wrapper.Unit#UNIT - UnitLife = Unit:GetLife() - else - local UnitLifeTotal = 0 - for UnitID, Unit in pairs( Units ) do - local Unit = Unit -- Wrapper.Unit#UNIT - UnitLifeTotal = UnitLifeTotal + Unit:GetLife() - end - UnitLife = UnitLifeTotal / #Units - end - return UnitLife - end - - return nil -end - ---- Returns the initial health. --- @param #CONTROLLABLE self --- @return #number The controllable health value (unit or group average). --- @return #nil The controllable is not existing or alive. -function CONTROLLABLE:GetLife0() - self:F2( self.ControllableName ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local UnitLife = 0 - local Units = self:GetUnits() - if #Units == 1 then - local Unit = Units[1] -- Wrapper.Unit#UNIT - UnitLife = Unit:GetLife0() - else - local UnitLifeTotal = 0 - for UnitID, Unit in pairs( Units ) do - local Unit = Unit -- Wrapper.Unit#UNIT - UnitLifeTotal = UnitLifeTotal + Unit:GetLife0() - end - UnitLife = UnitLifeTotal / #Units - end - return UnitLife - end - - return nil -end - ---- Returns relative amount of fuel (from 0.0 to 1.0) the unit has in its internal tanks. --- This method returns nil to ensure polymorphic behaviour! This method needs to be overridden by GROUP or UNIT. --- @param #CONTROLLABLE self --- @return #nil The CONTROLLABLE is not existing or alive. -function CONTROLLABLE:GetFuel() - self:F( self.ControllableName ) - - return nil -end - - - - --- Tasks - ---- Clear all tasks from the controllable. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE -function CONTROLLABLE:ClearTasks() - self:F2() - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local Controller = self:_GetController() - Controller:resetTask() - return self - end - - return nil -end - - ---- Popping current Task from the controllable. --- @param #CONTROLLABLE self --- @return Wrapper.Controllable#CONTROLLABLE self -function CONTROLLABLE:PopCurrentTask() - self:F2() - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local Controller = self:_GetController() - Controller:popTask() - return self - end - - return nil -end - ---- Pushing Task on the queue from the controllable. --- @param #CONTROLLABLE self --- @return Wrapper.Controllable#CONTROLLABLE self -function CONTROLLABLE:PushTask( DCSTask, WaitTime ) - self:F2() - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local Controller = self:_GetController() - - -- When a controllable SPAWNs, it takes about a second to get the controllable in the simulator. Setting tasks to unspawned controllables provides unexpected results. - -- Therefore we schedule the functions to set the mission and options for the Controllable. - -- Controller:pushTask( DCSTask ) - - if WaitTime then - self.TaskScheduler:Schedule( Controller, Controller.pushTask, { DCSTask }, WaitTime ) - else - Controller:pushTask( DCSTask ) - end - - return self - end - - return nil -end - ---- Clearing the Task Queue and Setting the Task on the queue from the controllable. --- @param #CONTROLLABLE self --- @return Wrapper.Controllable#CONTROLLABLE self -function CONTROLLABLE:SetTask( DCSTask, WaitTime ) - self:F2( { DCSTask = DCSTask } ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - - local DCSControllableName = self:GetName() - - -- When a controllable SPAWNs, it takes about a second to get the controllable in the simulator. Setting tasks to unspawned controllables provides unexpected results. - -- Therefore we schedule the functions to set the mission and options for the Controllable. - -- Controller.setTask( Controller, DCSTask ) - - local function SetTask( Controller, DCSTask ) - if self and self:IsAlive() then - local Controller = self:_GetController() - Controller:setTask( DCSTask ) - else - BASE:E( DCSControllableName .. " is not alive anymore. Cannot set DCSTask " .. DCSTask ) - end - end - - if not WaitTime or WaitTime == 0 then - SetTask( self, DCSTask ) - else - self.TaskScheduler:Schedule( self, SetTask, { DCSTask }, WaitTime ) - end - - return self - end - - return nil -end - ---- Checking the Task Queue of the controllable. Returns false if no task is on the queue. true if there is a task. --- @param #CONTROLLABLE self --- @return Wrapper.Controllable#CONTROLLABLE self -function CONTROLLABLE:HasTask() --R2.2 - - local HasTaskResult = false - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - - local Controller = self:_GetController() - HasTaskResult = Controller:hasTask() - end - - return HasTaskResult -end - - ---- Return a condition section for a controlled task. --- @param #CONTROLLABLE self --- @param Dcs.DCSTime#Time time --- @param #string userFlag --- @param #boolean userFlagValue --- @param #string condition --- @param Dcs.DCSTime#Time duration --- @param #number lastWayPoint --- return Dcs.DCSTasking.Task#Task -function CONTROLLABLE:TaskCondition( time, userFlag, userFlagValue, condition, duration, lastWayPoint ) - self:F2( { time, userFlag, userFlagValue, condition, duration, lastWayPoint } ) - - local DCSStopCondition = {} - DCSStopCondition.time = time - DCSStopCondition.userFlag = userFlag - DCSStopCondition.userFlagValue = userFlagValue - DCSStopCondition.condition = condition - DCSStopCondition.duration = duration - DCSStopCondition.lastWayPoint = lastWayPoint - - self:T3( { DCSStopCondition } ) - return DCSStopCondition -end - ---- Return a Controlled Task taking a Task and a TaskCondition. --- @param #CONTROLLABLE self --- @param Dcs.DCSTasking.Task#Task DCSTask --- @param #DCSStopCondition DCSStopCondition --- @return Dcs.DCSTasking.Task#Task -function CONTROLLABLE:TaskControlled( DCSTask, DCSStopCondition ) - self:F2( { DCSTask, DCSStopCondition } ) - - local DCSTaskControlled - - DCSTaskControlled = { - id = 'ControlledTask', - params = { - task = DCSTask, - stopCondition = DCSStopCondition - } - } - - self:T3( { DCSTaskControlled } ) - return DCSTaskControlled -end - ---- Return a Combo Task taking an array of Tasks. --- @param #CONTROLLABLE self --- @param Dcs.DCSTasking.Task#TaskArray DCSTasks Array of @{DCSTasking.Task#Task} --- @return Dcs.DCSTasking.Task#Task -function CONTROLLABLE:TaskCombo( DCSTasks ) - self:F2( { DCSTasks } ) - - local DCSTaskCombo - - DCSTaskCombo = { - id = 'ComboTask', - params = { - tasks = DCSTasks - } - } - - for TaskID, Task in ipairs( DCSTasks ) do - self:T( Task ) - end - - self:T3( { DCSTaskCombo } ) - return DCSTaskCombo -end - ---- Return a WrappedAction Task taking a Command. --- @param #CONTROLLABLE self --- @param Dcs.DCSCommand#Command DCSCommand --- @return Dcs.DCSTasking.Task#Task -function CONTROLLABLE:TaskWrappedAction( DCSCommand, Index ) - self:F2( { DCSCommand } ) - - local DCSTaskWrappedAction - - DCSTaskWrappedAction = { - id = "WrappedAction", - enabled = true, - number = Index or 1, - auto = false, - params = { - action = DCSCommand, - }, - } - - self:T3( { DCSTaskWrappedAction } ) - return DCSTaskWrappedAction -end - ---- Set a Task at a Waypoint using a Route list. --- @param #CONTROLLABLE self --- @param #table Waypoint The Waypoint! --- @param Dcs.DCSTasking.Task#Task Task The Task structure to be executed! --- @return Dcs.DCSTasking.Task#Task -function CONTROLLABLE:SetTaskWaypoint( Waypoint, Task ) - - Waypoint.task = self:TaskCombo( { Task } ) - - self:T3( { Waypoint.task } ) - return Waypoint.task -end - - - - ---- Executes a command action --- @param #CONTROLLABLE self --- @param Dcs.DCSCommand#Command DCSCommand --- @return #CONTROLLABLE self -function CONTROLLABLE:SetCommand( DCSCommand ) - self:F2( DCSCommand ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local Controller = self:_GetController() - Controller:setCommand( DCSCommand ) - return self - end - - return nil -end - ---- Perform a switch waypoint command --- @param #CONTROLLABLE self --- @param #number FromWayPoint --- @param #number ToWayPoint --- @return Dcs.DCSTasking.Task#Task --- @usage --- --- This test demonstrates the use(s) of the SwitchWayPoint method of the GROUP class. --- HeliGroup = GROUP:FindByName( "Helicopter" ) --- --- --- Route the helicopter back to the FARP after 60 seconds. --- -- We use the SCHEDULER class to do this. --- SCHEDULER:New( nil, --- function( HeliGroup ) --- local CommandRTB = HeliGroup:CommandSwitchWayPoint( 2, 8 ) --- HeliGroup:SetCommand( CommandRTB ) --- end, { HeliGroup }, 90 --- ) -function CONTROLLABLE:CommandSwitchWayPoint( FromWayPoint, ToWayPoint ) - self:F2( { FromWayPoint, ToWayPoint } ) - - local CommandSwitchWayPoint = { - id = 'SwitchWaypoint', - params = { - fromWaypointIndex = FromWayPoint, - goToWaypointIndex = ToWayPoint, - }, - } - - self:T3( { CommandSwitchWayPoint } ) - return CommandSwitchWayPoint -end - ---- Create a stop route command, which returns a string containing the command. --- Use the result in the method @{#CONTROLLABLE.SetCommand}(). --- A value of true will make the ground group stop, a value of false will make it continue. --- Note that this can only work on GROUP level, although individual UNITs can be commanded, the whole GROUP will react. --- --- Example missions: --- --- * GRP-310 --- --- @param #CONTROLLABLE self --- @param #boolean StopRoute true if the ground unit needs to stop, false if it needs to continue to move. --- @return Dcs.DCSTasking.Task#Task -function CONTROLLABLE:CommandStopRoute( StopRoute ) - self:F2( { StopRoute } ) - - local CommandStopRoute = { - id = 'StopRoute', - params = { - value = StopRoute, - }, - } - - self:T3( { CommandStopRoute } ) - return CommandStopRoute -end - - --- TASKS FOR AIR CONTROLLABLES - - ---- (AIR) Attack a Controllable. --- @param #CONTROLLABLE self --- @param Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. --- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param Dcs.DCSTypes#Distance Altitude (optional) Desired attack start altitude. Controllable/aircraft will make its attacks from the altitude. If the altitude is too low or too high to use weapon aircraft/controllable will choose closest altitude to the desired attack start altitude. If the desired altitude is defined controllable/aircraft will not attack from safe altitude. --- @param #boolean AttackQtyLimit (optional) The flag determines how to interpret attackQty parameter. If the flag is true then attackQty is a limit on maximal attack quantity for "AttackGroup" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) - self:F2( { self.ControllableName, AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) - - -- AttackGroup = { - -- id = 'AttackGroup', - -- params = { - -- groupId = Group.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend, - -- attackQty = number, - -- directionEnabled = boolean, - -- direction = Azimuth, - -- altitudeEnabled = boolean, - -- altitude = Distance, - -- attackQtyLimit = boolean, - -- } - -- } - - local DirectionEnabled = nil - if Direction then - DirectionEnabled = true - end - - local AltitudeEnabled = nil - if Altitude then - AltitudeEnabled = true - end - - local DCSTask - DCSTask = { id = 'AttackGroup', - params = { - groupId = AttackGroup:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - directionEnabled = DirectionEnabled, - direction = Direction, - altitudeEnabled = AltitudeEnabled, - altitude = Altitude, - attackQtyLimit = AttackQtyLimit, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (AIR) Attack the Unit. --- @param #CONTROLLABLE self --- @param Wrapper.Unit#UNIT AttackUnit The UNIT. --- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param #number Altitude (optional) The altitude from where to attack. --- @param #boolean Visible (optional) not a clue. --- @param #number WeaponType (optional) The WeaponType. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskAttackUnit( AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, WeaponType ) - self:F2( { self.ControllableName, AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, WeaponType } ) - - local DCSTask - DCSTask = { - id = 'AttackUnit', - params = { - unitId = AttackUnit:GetID(), - groupAttack = GroupAttack or false, - visible = Visible or false, - expend = WeaponExpend or "Auto", - directionEnabled = Direction and true or false, - direction = Direction, - altitudeEnabled = Altitude and true or false, - altitude = Altitude or 30, - attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, - weaponType = WeaponType - } - } - - self:T3( DCSTask ) - - return DCSTask -end - - ---- (AIR) Delivering weapon at the point on the ground. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. --- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param #number Altitude (optional) The altitude from where to attack. --- @param #number WeaponType (optional) The WeaponType. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskBombing( Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType ) - self:F2( { self.ControllableName, Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType } ) - - local DCSTask - DCSTask = { - id = 'Bombing', - params = { - point = Vec2, - groupAttack = GroupAttack or false, - expend = WeaponExpend or "Auto", - attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, - directionEnabled = Direction and true or false, - direction = Direction, - altitudeEnabled = Altitude and true or false, - altitude = Altitude or 30, - weaponType = WeaponType, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (AIR) Attacking the map object (building, structure, e.t.c). --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. --- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param #number Altitude (optional) The altitude from where to attack. --- @param #number WeaponType (optional) The WeaponType. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskAttackMapObject( Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType ) - self:F2( { self.ControllableName, Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType } ) - - local DCSTask - DCSTask = { - id = 'AttackMapObject', - params = { - point = Vec2, - groupAttack = GroupAttack or false, - expend = WeaponExpend or "Auto", - attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, - directionEnabled = Direction and true or false, - direction = Direction, - altitudeEnabled = Altitude and true or false, - altitude = Altitude or 30, - weaponType = WeaponType, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Point The point to hold the position. --- @param #number Altitude The altitude to hold the position. --- @param #number Speed The speed flying when holding the position. --- @return #CONTROLLABLE self -function CONTROLLABLE:TaskOrbitCircleAtVec2( Point, Altitude, Speed ) - self:F2( { self.ControllableName, Point, Altitude, Speed } ) - - -- pattern = enum AI.Task.OribtPattern, - -- point = Vec2, - -- point2 = Vec2, - -- speed = Distance, - -- altitude = Distance - - local LandHeight = land.getHeight( Point ) - - self:T3( { LandHeight } ) - - local DCSTask = { id = 'Orbit', - params = { pattern = AI.Task.OrbitPattern.CIRCLE, - point = Point, - speed = Speed, - altitude = Altitude + LandHeight - } - } - - - -- local AITask = { id = 'ControlledTask', - -- params = { task = { id = 'Orbit', - -- params = { pattern = AI.Task.OrbitPattern.CIRCLE, - -- point = Point, - -- speed = Speed, - -- altitude = Altitude + LandHeight - -- } - -- }, - -- stopCondition = { duration = Duration - -- } - -- } - -- } - -- ) - - return DCSTask -end - ---- (AIR) Orbit at the current position of the first unit of the controllable at a specified alititude. --- @param #CONTROLLABLE self --- @param #number Altitude The altitude to hold the position. --- @param #number Speed The speed flying when holding the position. --- @return #CONTROLLABLE self -function CONTROLLABLE:TaskOrbitCircle( Altitude, Speed ) - self:F2( { self.ControllableName, Altitude, Speed } ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local ControllablePoint = self:GetVec2() - return self:TaskOrbitCircleAtVec2( ControllablePoint, Altitude, Speed ) - end - - return nil -end - - - ---- (AIR) Hold position at the current position of the first unit of the controllable. --- @param #CONTROLLABLE self --- @param #number Duration The maximum duration in seconds to hold the position. --- @return #CONTROLLABLE self -function CONTROLLABLE:TaskHoldPosition() - self:F2( { self.ControllableName } ) - - return self:TaskOrbitCircle( 30, 10 ) -end - - - - - - ---- (AIR) Delivering weapon on the runway. --- @param #CONTROLLABLE self --- @param Wrapper.Airbase#AIRBASE Airbase Airbase to attack. --- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskBombingRunway( Airbase, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack ) - self:F2( { self.ControllableName, Airbase, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack } ) - --- BombingRunway = { --- id = 'BombingRunway', --- params = { --- runwayId = AirdromeId, --- weaponType = number, --- expend = enum AI.Task.WeaponExpend, --- attackQty = number, --- direction = Azimuth, --- controllableAttack = boolean, --- } --- } - - local DCSTask - DCSTask = { id = 'BombingRunway', - params = { - point = Airbase:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - direction = Direction, - controllableAttack = ControllableAttack, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Refueling from the nearest tanker. No parameters. --- @param #CONTROLLABLE self --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskRefueling() - self:F2( { self.ControllableName } ) - --- Refueling = { --- id = 'Refueling', --- params = {} --- } - - local DCSTask - DCSTask = { id = 'Refueling', - params = { - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR HELICOPTER) Landing at the ground. For helicopters only. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Point The point where to land. --- @param #number Duration The duration in seconds to stay on the ground. --- @return #CONTROLLABLE self -function CONTROLLABLE:TaskLandAtVec2( Point, Duration ) - self:F2( { self.ControllableName, Point, Duration } ) - --- Land = { --- id= 'Land', --- params = { --- point = Vec2, --- durationFlag = boolean, --- duration = Time --- } --- } - - local DCSTask - if Duration and Duration > 0 then - DCSTask = { id = 'Land', - params = { - point = Point, - durationFlag = true, - duration = Duration, - }, - } - else - DCSTask = { id = 'Land', - params = { - point = Point, - durationFlag = false, - }, - } - end - - self:T3( DCSTask ) - return DCSTask -end - ---- (AIR) Land the controllable at a @{Zone#ZONE_RADIUS). --- @param #CONTROLLABLE self --- @param Core.Zone#ZONE Zone The zone where to land. --- @param #number Duration The duration in seconds to stay on the ground. --- @return #CONTROLLABLE self -function CONTROLLABLE:TaskLandAtZone( Zone, Duration, RandomPoint ) - self:F2( { self.ControllableName, Zone, Duration, RandomPoint } ) - - local Point - if RandomPoint then - Point = Zone:GetRandomVec2() - else - Point = Zone:GetVec2() - end - - local DCSTask = self:TaskLandAtVec2( Point, Duration ) - - self:T3( DCSTask ) - return DCSTask -end - - - ---- (AIR) Following another airborne controllable. --- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. --- If another controllable is on land the unit / controllable will orbit around. --- @param #CONTROLLABLE self --- @param Wrapper.Controllable#CONTROLLABLE FollowControllable The controllable to be followed. --- @param Dcs.DCSTypes#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. --- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskFollow( FollowControllable, Vec3, LastWaypointIndex ) - self:F2( { self.ControllableName, FollowControllable, Vec3, LastWaypointIndex } ) - --- Follow = { --- id = 'Follow', --- params = { --- groupId = Group.ID, --- pos = Vec3, --- lastWptIndexFlag = boolean, --- lastWptIndex = number --- } --- } - - local LastWaypointIndexFlag = false - if LastWaypointIndex then - LastWaypointIndexFlag = true - end - - local DCSTask - DCSTask = { - id = 'Follow', - params = { - groupId = FollowControllable:GetID(), - pos = Vec3, - lastWptIndexFlag = LastWaypointIndexFlag, - lastWptIndex = LastWaypointIndex - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Escort another airborne controllable. --- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. --- The unit / controllable will also protect that controllable from threats of specified types. --- @param #CONTROLLABLE self --- @param Wrapper.Controllable#CONTROLLABLE EscortControllable The controllable to be escorted. --- @param Dcs.DCSTypes#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. --- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. --- @param #number EngagementDistanceMax Maximal distance from escorted controllable to threat. If the threat is already engaged by escort escort will disengage if the distance becomes greater than 1.5 * engagementDistMax. --- @param Dcs.DCSTypes#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes ) - self:F2( { self.ControllableName, FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes } ) - --- Escort = { --- id = 'Escort', --- params = { --- groupId = Group.ID, --- pos = Vec3, --- lastWptIndexFlag = boolean, --- lastWptIndex = number, --- engagementDistMax = Distance, --- targetTypes = array of AttributeName, --- } --- } - - local LastWaypointIndexFlag = false - if LastWaypointIndex then - LastWaypointIndexFlag = true - end - - local DCSTask - DCSTask = { id = 'Escort', - params = { - groupId = FollowControllable:GetID(), - pos = Vec3, - lastWptIndexFlag = LastWaypointIndexFlag, - lastWptIndex = LastWaypointIndex, - engagementDistMax = EngagementDistance, - targetTypes = TargetTypes, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - --- GROUND TASKS - ---- (GROUND) Fire at a VEC2 point until ammunition is finished. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Vec2 The point to fire at. --- @param Dcs.DCSTypes#Distance Radius The radius of the zone to deploy the fire at. --- @param #number AmmoCount (optional) Quantity of ammunition to expand (omit to fire until ammunition is depleted). --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount ) - self:F2( { self.ControllableName, Vec2, Radius, AmmoCount } ) - - -- FireAtPoint = { - -- id = 'FireAtPoint', - -- params = { - -- point = Vec2, - -- radius = Distance, - -- expendQty = number, - -- expendQtyEnabled = boolean, - -- } - -- } - - local DCSTask - DCSTask = { id = 'FireAtPoint', - params = { - point = Vec2, - radius = Radius, - expendQty = 100, -- dummy value - expendQtyEnabled = false, - } - } - - if AmmoCount then - DCSTask.params.expendQty = AmmoCount - DCSTask.params.expendQtyEnabled = true - end - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (GROUND) Hold ground controllable from moving. --- @param #CONTROLLABLE self --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskHold() - self:F2( { self.ControllableName } ) - --- Hold = { --- id = 'Hold', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'Hold', - params = { - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - --- TASKS FOR AIRBORNE AND GROUND UNITS/CONTROLLABLES - ---- (AIR + GROUND) The task makes the controllable/unit a FAC and orders the FAC to control the target (enemy ground controllable) destruction. --- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. --- If the task is assigned to the controllable lead unit will be a FAC. --- @param #CONTROLLABLE self --- @param Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE. --- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param Dcs.DCSTypes#AI.Task.Designation Designation (optional) Designation type. --- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskFAC_AttackGroup( AttackGroup, WeaponType, Designation, Datalink ) - self:F2( { self.ControllableName, AttackGroup, WeaponType, Designation, Datalink } ) - --- FAC_AttackGroup = { --- id = 'FAC_AttackGroup', --- params = { --- groupId = Group.ID, --- weaponType = number, --- designation = enum AI.Task.Designation, --- datalink = boolean --- } --- } - - local DCSTask - DCSTask = { id = 'FAC_AttackGroup', - params = { - groupId = AttackGroup:GetID(), - weaponType = WeaponType, - designation = Designation, - datalink = Datalink, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - --- EN-ACT_ROUTE TASKS FOR AIRBORNE CONTROLLABLES - ---- (AIR) Engaging targets of defined types. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Distance Distance Maximal distance from the target to a route leg. If the target is on a greater distance it will be ignored. --- @param Dcs.DCSTypes#AttributeNameArray TargetTypes Array of target categories allowed to engage. --- @param #number Priority All enroute tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEngageTargets( Distance, TargetTypes, Priority ) - self:F2( { self.ControllableName, Distance, TargetTypes, Priority } ) - --- EngageTargets ={ --- id = 'EngageTargets', --- params = { --- maxDist = Distance, --- targetTypes = array of AttributeName, --- priority = number --- } --- } - - local DCSTask - DCSTask = { id = 'EngageTargets', - params = { - maxDist = Distance, - targetTypes = TargetTypes, - priority = Priority - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - - ---- (AIR) Engaging a targets of defined types at circle-shaped zone. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Vec2 2D-coordinates of the zone. --- @param Dcs.DCSTypes#Distance Radius Radius of the zone. --- @param Dcs.DCSTypes#AttributeNameArray TargetTypes Array of target categories allowed to engage. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEngageTargetsInZone( Vec2, Radius, TargetTypes, Priority ) - self:F2( { self.ControllableName, Vec2, Radius, TargetTypes, Priority } ) - --- EngageTargetsInZone = { --- id = 'EngageTargetsInZone', --- params = { --- point = Vec2, --- zoneRadius = Distance, --- targetTypes = array of AttributeName, --- priority = number --- } --- } - - local DCSTask - DCSTask = { id = 'EngageTargetsInZone', - params = { - point = Vec2, - zoneRadius = Radius, - targetTypes = TargetTypes, - priority = Priority - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Engaging a controllable. The task does not assign the target controllable to the unit/controllable to attack now; it just allows the unit/controllable to engage the target controllable as well as other assigned targets. --- @param #CONTROLLABLE self --- @param Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param Dcs.DCSTypes#Distance Altitude (optional) Desired attack start altitude. Controllable/aircraft will make its attacks from the altitude. If the altitude is too low or too high to use weapon aircraft/controllable will choose closest altitude to the desired attack start altitude. If the desired altitude is defined controllable/aircraft will not attack from safe altitude. --- @param #boolean AttackQtyLimit (optional) The flag determines how to interpret attackQty parameter. If the flag is true then attackQty is a limit on maximal attack quantity for "AttackGroup" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEngageGroup( AttackGroup, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) - self:F2( { self.ControllableName, AttackGroup, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) - - -- EngageControllable = { - -- id = 'EngageControllable ', - -- params = { - -- groupId = Group.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend, - -- attackQty = number, - -- directionEnabled = boolean, - -- direction = Azimuth, - -- altitudeEnabled = boolean, - -- altitude = Distance, - -- attackQtyLimit = boolean, - -- priority = number, - -- } - -- } - - local DirectionEnabled = nil - if Direction then - DirectionEnabled = true - end - - local AltitudeEnabled = nil - if Altitude then - AltitudeEnabled = true - end - - local DCSTask - DCSTask = { id = 'EngageControllable', - params = { - groupId = AttackGroup:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - directionEnabled = DirectionEnabled, - direction = Direction, - altitudeEnabled = AltitudeEnabled, - altitude = Altitude, - attackQtyLimit = AttackQtyLimit, - priority = Priority, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Search and attack the Unit. --- @param #CONTROLLABLE self --- @param Wrapper.Unit#UNIT EngageUnit The UNIT. --- @param #number Priority (optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param Dcs.DCSTypes#Distance Altitude (optional) Desired altitude to perform the unit engagement. --- @param #boolean Visible (optional) Unit must be visible. --- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEngageUnit( EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack ) - self:F2( { self.ControllableName, EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack } ) - - -- EngageUnit = { - -- id = 'EngageUnit', - -- params = { - -- unitId = Unit.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend - -- attackQty = number, - -- direction = Azimuth, - -- attackQtyLimit = boolean, - -- controllableAttack = boolean, - -- priority = number, - -- } - -- } - - local DCSTask - DCSTask = { id = 'EngageUnit', - params = { - unitId = EngageUnit:GetID(), - priority = Priority or 1, - groupAttack = GroupAttack or false, - visible = Visible or false, - expend = WeaponExpend or "Auto", - directionEnabled = Direction and true or false, - direction = Direction, - altitudeEnabled = Altitude and true or false, - altitude = Altitude, - attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, - controllableAttack = ControllableAttack, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - - ---- (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters. --- @param #CONTROLLABLE self --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskAWACS( ) - self:F2( { self.ControllableName } ) - --- AWACS = { --- id = 'AWACS', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'AWACS', - params = { - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Aircraft will act as a tanker for friendly units. No parameters. --- @param #CONTROLLABLE self --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskTanker( ) - self:F2( { self.ControllableName } ) - --- Tanker = { --- id = 'Tanker', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'Tanker', - params = { - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - --- En-route tasks for ground units/controllables - ---- (GROUND) Ground unit (EW-radar) will act as an EWR for friendly units (will provide them with information about contacts). No parameters. --- @param #CONTROLLABLE self --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEWR( ) - self:F2( { self.ControllableName } ) - --- EWR = { --- id = 'EWR', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'EWR', - params = { - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - --- En-route tasks for airborne and ground units/controllables - ---- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose the target (enemy ground controllable) as well as other assigned targets. --- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. --- If the task is assigned to the controllable lead unit will be a FAC. --- @param #CONTROLLABLE self --- @param Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param Dcs.DCSTypes#AI.Task.Designation Designation (optional) Designation type. --- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskFAC_EngageGroup( AttackGroup, Priority, WeaponType, Designation, Datalink ) - self:F2( { self.ControllableName, AttackGroup, WeaponType, Priority, Designation, Datalink } ) - --- FAC_EngageControllable = { --- id = 'FAC_EngageControllable', --- params = { --- groupId = Group.ID, --- weaponType = number, --- designation = enum AI.Task.Designation, --- datalink = boolean, --- priority = number, --- } --- } - - local DCSTask - DCSTask = { id = 'FAC_EngageControllable', - params = { - groupId = AttackGroup:GetID(), - weaponType = WeaponType, - designation = Designation, - datalink = Datalink, - priority = Priority, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose a targets (enemy ground controllable) around as well as other assigned targets. --- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. --- If the task is assigned to the controllable lead unit will be a FAC. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Distance Radius The maximal distance from the FAC to a target. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskFAC( Radius, Priority ) - self:F2( { self.ControllableName, Radius, Priority } ) - --- FAC = { --- id = 'FAC', --- params = { --- radius = Distance, --- priority = number --- } --- } - - local DCSTask - DCSTask = { id = 'FAC', - params = { - radius = Radius, - priority = Priority - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - - - ---- (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Point The point where to wait. --- @param #number Duration The duration in seconds to wait. --- @param #CONTROLLABLE EmbarkingControllable The controllable to be embarked. --- @return Dcs.DCSTasking.Task#Task The DCS task structure -function CONTROLLABLE:TaskEmbarking( Point, Duration, EmbarkingControllable ) - self:F2( { self.ControllableName, Point, Duration, EmbarkingControllable.DCSControllable } ) - - local DCSTask - DCSTask = { id = 'Embarking', - params = { x = Point.x, - y = Point.y, - duration = Duration, - controllablesForEmbarking = { EmbarkingControllable.ControllableID }, - durationFlag = true, - distributionFlag = false, - distribution = {}, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (GROUND) Embark to a Transport landed at a location. - ---- Move to a defined Vec2 Point, and embark to a controllable when arrived within a defined Radius. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Point The point where to wait. --- @param #number Radius The radius of the embarking zone around the Point. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskEmbarkToTransport( Point, Radius ) - self:F2( { self.ControllableName, Point, Radius } ) - - local DCSTask --Dcs.DCSTasking.Task#Task - DCSTask = { id = 'EmbarkToTransport', - params = { x = Point.x, - y = Point.y, - zoneRadius = Radius, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- This creates a Task element, with an action to call a function as part of a Wrapped Task. --- This Task can then be embedded at a Waypoint by calling the method @{#CONTROLLABLE.SetTaskWaypoint}. --- @param #CONTROLLABLE self --- @param #string FunctionString The function name embedded as a string that will be called. --- @param ... The variable arguments passed to the function when called! These arguments can be of any type! --- @return #CONTROLLABLE --- @usage --- --- local ZoneList = { --- ZONE:New( "ZONE1" ), --- ZONE:New( "ZONE2" ), --- ZONE:New( "ZONE3" ), --- ZONE:New( "ZONE4" ), --- ZONE:New( "ZONE5" ) --- } --- --- GroundGroup = GROUP:FindByName( "Vehicle" ) --- --- --- @param Wrapper.Group#GROUP GroundGroup --- function RouteToZone( Vehicle, ZoneRoute ) --- --- local Route = {} --- --- Vehicle:E( { ZoneRoute = ZoneRoute } ) --- --- Vehicle:MessageToAll( "Moving to zone " .. ZoneRoute:GetName(), 10 ) --- --- -- Get the current coordinate of the Vehicle --- local FromCoord = Vehicle:GetCoordinate() --- --- -- Select a random Zone and get the Coordinate of the new Zone. --- local RandomZone = ZoneList[ math.random( 1, #ZoneList ) ] -- Core.Zone#ZONE --- local ToCoord = RandomZone:GetCoordinate() --- --- -- Create a "ground route point", which is a "point" structure that can be given as a parameter to a Task --- Route[#Route+1] = FromCoord:WaypointGround( 72 ) --- Route[#Route+1] = ToCoord:WaypointGround( 60, "Vee" ) --- --- local TaskRouteToZone = Vehicle:TaskFunction( "RouteToZone", RandomZone ) --- --- Vehicle:SetTaskWaypoint( Route, #Route, TaskRouteToZone ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. --- --- Vehicle:Route( Route, math.random( 10, 20 ) ) -- Move after a random seconds to the Route. See the Route method for details. --- --- end --- --- RouteToZone( GroundGroup, ZoneList[1] ) --- -function CONTROLLABLE:TaskFunction( FunctionString, ... ) - self:F2( { FunctionString, arg } ) - - local DCSTask - - local DCSScript = {} - DCSScript[#DCSScript+1] = "local MissionControllable = GROUP:Find( ... ) " - - if arg and arg.n > 0 then - local ArgumentKey = '_' .. tostring( arg ):match("table: (.*)") - self:SetState( self, ArgumentKey, arg ) - DCSScript[#DCSScript+1] = "local Arguments = MissionControllable:GetState( MissionControllable, '" .. ArgumentKey .. "' ) " - --DCSScript[#DCSScript+1] = "MissionControllable:ClearState( MissionControllable, '" .. ArgumentKey .. "' ) " - DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable, unpack( Arguments ) )" - else - DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable )" - end - - DCSTask = self:TaskWrappedAction( - self:CommandDoScript( - table.concat( DCSScript ) - ) - ) - - self:T( DCSTask ) - - return DCSTask - -end - - - ---- (AIR + GROUND) Return a mission task from a mission template. --- @param #CONTROLLABLE self --- @param #table TaskMission A table containing the mission task. --- @return Dcs.DCSTasking.Task#Task -function CONTROLLABLE:TaskMission( TaskMission ) - self:F2( Points ) - - local DCSTask - DCSTask = { id = 'Mission', params = { TaskMission, }, } - - self:T3( { DCSTask } ) - return DCSTask -end - - -do -- Patrol methods - - --- (GROUND) Patrol iteratively using the waypoints the for the (parent) group. - -- @param #CONTROLLABLE self - -- @return #CONTROLLABLE - function CONTROLLABLE:PatrolRoute() - - local PatrolGroup = self -- Wrapper.Group#GROUP - - if not self:IsInstanceOf( "GROUP" ) then - PatrolGroup = self:GetGroup() -- Wrapper.Group#GROUP - end - - self:E( { PatrolGroup = PatrolGroup:GetName() } ) - - if PatrolGroup:IsGround() or PatrolGroup:IsShip() then - - local Waypoints = PatrolGroup:GetTemplateRoutePoints() - - -- Calculate the new Route. - local FromCoord = PatrolGroup:GetCoordinate() - local From = FromCoord:WaypointGround( 120 ) - - table.insert( Waypoints, 1, From ) - - local TaskRoute = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolRoute" ) - - self:E({Waypoints = Waypoints}) - local Waypoint = Waypoints[#Waypoints] - PatrolGroup:SetTaskWaypoint( Waypoint, TaskRoute ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. - - PatrolGroup:Route( Waypoints ) -- Move after a random seconds to the Route. See the Route method for details. - end - end - - --- (GROUND) Patrol randomly to the waypoints the for the (parent) group. - -- A random waypoint will be picked and the group will move towards that point. - -- @param #CONTROLLABLE self - -- @return #CONTROLLABLE - function CONTROLLABLE:PatrolRouteRandom( Speed, Formation, ToWaypoint ) - - local PatrolGroup = self -- Wrapper.Group#GROUP - - if not self:IsInstanceOf( "GROUP" ) then - PatrolGroup = self:GetGroup() -- Wrapper.Group#GROUP - end - - self:E( { PatrolGroup = PatrolGroup:GetName() } ) - - if PatrolGroup:IsGround() or PatrolGroup:IsShip() then - - local Waypoints = PatrolGroup:GetTemplateRoutePoints() - - -- Calculate the new Route. - local FromCoord = PatrolGroup:GetCoordinate() - local FromWaypoint = 1 - if ToWaypoint then - FromWaypoint = ToWaypoint - end - - -- Loop until a waypoint has been found that is not the same as the current waypoint. - -- Otherwise the object zon't move or drive in circles and the algorithm would not do exactly - -- what it is supposed to do, which is making groups drive around. - local ToWaypoint - repeat - -- Select a random waypoint and check if it is not the same waypoint as where the object is about. - ToWaypoint = math.random( 1, #Waypoints ) - until( ToWaypoint ~= FromWaypoint ) - self:E( { FromWaypoint = FromWaypoint, ToWaypoint = ToWaypoint } ) - - local Waypoint = Waypoints[ToWaypoint] -- Select random waypoint. - local ToCoord = COORDINATE:NewFromVec2( { x = Waypoint.x, y = Waypoint.y } ) - -- Create a "ground route point", which is a "point" structure that can be given as a parameter to a Task - local Route = {} - Route[#Route+1] = FromCoord:WaypointGround( 0 ) - Route[#Route+1] = ToCoord:WaypointGround( Speed, Formation ) - - - local TaskRouteToZone = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolRouteRandom", Speed, Formation, ToWaypoint ) - - PatrolGroup:SetTaskWaypoint( Route[#Route], TaskRouteToZone ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. - - PatrolGroup:Route( Route, 1 ) -- Move after a random seconds to the Route. See the Route method for details. - end - end - - --- (GROUND) Patrol randomly to the waypoints the for the (parent) group. - -- A random waypoint will be picked and the group will move towards that point. - -- @param #CONTROLLABLE self - -- @return #CONTROLLABLE - function CONTROLLABLE:PatrolZones( ZoneList, Speed, Formation ) - - if not type( ZoneList ) == "table" then - ZoneList = { ZoneList } - end - - local PatrolGroup = self -- Wrapper.Group#GROUP - - if not self:IsInstanceOf( "GROUP" ) then - PatrolGroup = self:GetGroup() -- Wrapper.Group#GROUP - end - - self:E( { PatrolGroup = PatrolGroup:GetName() } ) - - if PatrolGroup:IsGround() or PatrolGroup:IsShip() then - - local Waypoints = PatrolGroup:GetTemplateRoutePoints() - local Waypoint = Waypoints[math.random( 1, #Waypoints )] -- Select random waypoint. - - -- Calculate the new Route. - local FromCoord = PatrolGroup:GetCoordinate() - - -- Select a random Zone and get the Coordinate of the new Zone. - local RandomZone = ZoneList[ math.random( 1, #ZoneList ) ] -- Core.Zone#ZONE - local ToCoord = RandomZone:GetRandomCoordinate( 10 ) - - -- Create a "ground route point", which is a "point" structure that can be given as a parameter to a Task - local Route = {} - Route[#Route+1] = FromCoord:WaypointGround( 120 ) - Route[#Route+1] = ToCoord:WaypointGround( Speed, Formation ) - - - local TaskRouteToZone = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolZones", ZoneList, Speed, Formation ) - - PatrolGroup:SetTaskWaypoint( Route[#Route], TaskRouteToZone ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. - - PatrolGroup:Route( Route, 1 ) -- Move after a random seconds to the Route. See the Route method for details. - end - end - -end - - ---- Return a Misson task to follow a given route defined by Points. --- @param #CONTROLLABLE self --- @param #table Points A table of route points. --- @return Dcs.DCSTasking.Task#Task -function CONTROLLABLE:TaskRoute( Points ) - self:F2( Points ) - - local DCSTask - DCSTask = { id = 'Mission', params = { route = { points = Points, }, }, } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (AIR + GROUND) Make the Controllable move to fly to a given point. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec3 Point The destination point in Vec3 format. --- @param #number Speed The speed to travel. --- @return #CONTROLLABLE self -function CONTROLLABLE:RouteToVec2( Point, Speed ) - self:F2( { Point, Speed } ) - - local ControllablePoint = self:GetUnit( 1 ):GetVec2() - - local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.y - PointFrom.type = "Turning Point" - PointFrom.action = "Turning Point" - PointFrom.speed = Speed - PointFrom.speed_locked = true - PointFrom.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local PointTo = {} - PointTo.x = Point.x - PointTo.y = Point.y - PointTo.type = "Turning Point" - PointTo.action = "Fly Over Point" - PointTo.speed = Speed - PointTo.speed_locked = true - PointTo.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - self:Route( Points ) - - return self -end - ---- (AIR + GROUND) Make the Controllable move to a given point. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec3 Point The destination point in Vec3 format. --- @param #number Speed The speed to travel. --- @return #CONTROLLABLE self -function CONTROLLABLE:RouteToVec3( Point, Speed ) - self:F2( { Point, Speed } ) - - local ControllableVec3 = self:GetUnit( 1 ):GetVec3() - - local PointFrom = {} - PointFrom.x = ControllableVec3.x - PointFrom.y = ControllableVec3.z - PointFrom.alt = ControllableVec3.y - PointFrom.alt_type = "BARO" - PointFrom.type = "Turning Point" - PointFrom.action = "Turning Point" - PointFrom.speed = Speed - PointFrom.speed_locked = true - PointFrom.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local PointTo = {} - PointTo.x = Point.x - PointTo.y = Point.z - PointTo.alt = Point.y - PointTo.alt_type = "BARO" - PointTo.type = "Turning Point" - PointTo.action = "Fly Over Point" - PointTo.speed = Speed - PointTo.speed_locked = true - PointTo.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - self:Route( Points ) - - return self -end - - - ---- Make the controllable to follow a given route. --- @param #CONTROLLABLE self --- @param #table Route A table of Route Points. --- @param #number DelaySeconds Wait for the specified seconds before executing the Route. --- @return #CONTROLLABLE The CONTROLLABLE. -function CONTROLLABLE:Route( Route, DelaySeconds ) - self:F2( Route ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local RouteTask = self:TaskRoute( Route ) -- Create a RouteTask, that will route the CONTROLLABLE to the Route. - self:SetTask( RouteTask, DelaySeconds or 1 ) -- Execute the RouteTask after the specified seconds (default is 1). - return self - end - - return nil -end - - ---- Make the GROUND Controllable to drive towards a specific point. --- @param #CONTROLLABLE self --- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to. --- @param #number Speed (optional) Speed in km/h. The default speed is 999 km/h. --- @param #string Formation (optional) The route point Formation, which is a text string that specifies exactly the Text in the Type of the route point, like "Vee", "Echelon Right". --- @param #number DelaySeconds Wait for the specified seconds before executing the Route. --- @return #CONTROLLABLE The CONTROLLABLE. -function CONTROLLABLE:RouteGroundTo( ToCoordinate, Speed, Formation, DelaySeconds ) - - local FromCoordinate = self:GetCoordinate() - - local FromWP = FromCoordinate:WaypointGround() - local ToWP = ToCoordinate:WaypointGround( Speed, Formation ) - - self:Route( { FromWP, ToWP }, DelaySeconds ) - - return self -end - - ---- Make the AIR Controllable fly towards a specific point. --- @param #CONTROLLABLE self --- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to. --- @param Core.Point#COORDINATE.RoutePointAltType AltType The altitude type. --- @param Core.Point#COORDINATE.RoutePointType Type The route point type. --- @param Core.Point#COORDINATE.RoutePointAction Action The route point action. --- @param #number Speed (optional) Speed in km/h. The default speed is 999 km/h. --- @param #number DelaySeconds Wait for the specified seconds before executing the Route. --- @return #CONTROLLABLE The CONTROLLABLE. -function CONTROLLABLE:RouteAirTo( ToCoordinate, AltType, Type, Action, Speed, DelaySeconds ) - - local FromCoordinate = self:GetCoordinate() - local FromWP = FromCoordinate:WaypointAir() - - local ToWP = ToCoordinate:WaypointAir( AltType, Type, Action, Speed ) - - self:Route( { FromWP, ToWP }, DelaySeconds ) - - return self -end - - ---- (AIR + GROUND) Route the controllable to a given zone. --- The controllable final destination point can be randomized. --- A speed can be given in km/h. --- A given formation can be given. --- @param #CONTROLLABLE self --- @param Core.Zone#ZONE Zone The zone where to route to. --- @param #boolean Randomize Defines whether to target point gets randomized within the Zone. --- @param #number Speed The speed. --- @param Base#FORMATION Formation The formation string. -function CONTROLLABLE:TaskRouteToZone( Zone, Randomize, Speed, Formation ) - self:F2( Zone ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - - local ControllablePoint = self:GetVec2() - - local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.y - PointFrom.type = "Turning Point" - PointFrom.action = Formation or "Cone" - PointFrom.speed = 20 / 1.6 - - - local PointTo = {} - local ZonePoint - - if Randomize then - ZonePoint = Zone:GetRandomVec2() - else - ZonePoint = Zone:GetVec2() - end - - PointTo.x = ZonePoint.x - PointTo.y = ZonePoint.y - PointTo.type = "Turning Point" - - if Formation then - PointTo.action = Formation - else - PointTo.action = "Cone" - end - - if Speed then - PointTo.speed = Speed - else - PointTo.speed = 20 / 1.6 - end - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - self:Route( Points ) - - return self - end - - return nil -end - ---- (GROUND) Route the controllable to a given Vec2. --- A speed can be given in km/h. --- A given formation can be given. --- @param #CONTROLLABLE self --- @param #Vec2 Vec2 The Vec2 where to route to. --- @param #number Speed The speed. --- @param Base#FORMATION Formation The formation string. -function CONTROLLABLE:TaskRouteToVec2( Vec2, Speed, Formation ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - - local ControllablePoint = self:GetVec2() - - local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.y - PointFrom.type = "Turning Point" - PointFrom.action = Formation or "Cone" - PointFrom.speed = 20 / 1.6 - - - local PointTo = {} - - PointTo.x = Vec2.x - PointTo.y = Vec2.y - PointTo.type = "Turning Point" - - if Formation then - PointTo.action = Formation - else - PointTo.action = "Cone" - end - - if Speed then - PointTo.speed = Speed - else - PointTo.speed = 60 / 3.6 - end - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - self:Route( Points ) - - return self - end - - return nil -end - - --- Commands - ---- Do Script command --- @param #CONTROLLABLE self --- @param #string DoScript --- @return #DCSCommand -function CONTROLLABLE:CommandDoScript( DoScript ) - - local DCSDoScript = { - id = "Script", - params = { - command = DoScript, - }, - } - - self:T3( DCSDoScript ) - return DCSDoScript -end - - ---- Return the mission template of the controllable. --- @param #CONTROLLABLE self --- @return #table The MissionTemplate --- TODO: Rework the method how to retrieve a template ... -function CONTROLLABLE:GetTaskMission() - self:F2( self.ControllableName ) - - return routines.utils.deepCopy( _DATABASE.Templates.Controllables[self.ControllableName].Template ) -end - ---- Return the mission route of the controllable. --- @param #CONTROLLABLE self --- @return #table The mission route defined by points. -function CONTROLLABLE:GetTaskRoute() - self:F2( self.ControllableName ) - - return routines.utils.deepCopy( _DATABASE.Templates.Controllables[self.ControllableName].Template.route.points ) -end - - - ---- Return the route of a controllable by using the @{Database#DATABASE} class. --- @param #CONTROLLABLE self --- @param #number Begin The route point from where the copy will start. The base route point is 0. --- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0. --- @param #boolean Randomize Randomization of the route, when true. --- @param #number Radius When randomization is on, the randomization is within the radius. -function CONTROLLABLE:CopyRoute( Begin, End, Randomize, Radius ) - self:F2( { Begin, End } ) - - local Points = {} - - -- Could be a Spawned Controllable - local ControllableName = string.match( self:GetName(), ".*#" ) - if ControllableName then - ControllableName = ControllableName:sub( 1, -2 ) - else - ControllableName = self:GetName() - end - - self:T3( { ControllableName } ) - - local Template = _DATABASE.Templates.Controllables[ControllableName].Template - - if Template then - if not Begin then - Begin = 0 - end - if not End then - End = 0 - end - - for TPointID = Begin + 1, #Template.route.points - End do - if Template.route.points[TPointID] then - Points[#Points+1] = routines.utils.deepCopy( Template.route.points[TPointID] ) - if Randomize then - if not Radius then - Radius = 500 - end - Points[#Points].x = Points[#Points].x + math.random( Radius * -1, Radius ) - Points[#Points].y = Points[#Points].y + math.random( Radius * -1, Radius ) - end - end - end - return Points - else - error( "Template not found for Controllable : " .. ControllableName ) - end - - return nil -end - - ---- Return the detected targets of the controllable. --- The optional parametes specify the detection methods that can be applied. --- If no detection method is given, the detection will use all the available methods by default. --- @param Wrapper.Controllable#CONTROLLABLE self --- @param #boolean DetectVisual (optional) --- @param #boolean DetectOptical (optional) --- @param #boolean DetectRadar (optional) --- @param #boolean DetectIRST (optional) --- @param #boolean DetectRWR (optional) --- @param #boolean DetectDLINK (optional) --- @return #table DetectedTargets -function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) - self:F2( self.ControllableName ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local DetectionVisual = ( DetectVisual and DetectVisual == true ) and Controller.Detection.VISUAL or nil - local DetectionOptical = ( DetectOptical and DetectOptical == true ) and Controller.Detection.OPTICAL or nil - local DetectionRadar = ( DetectRadar and DetectRadar == true ) and Controller.Detection.RADAR or nil - local DetectionIRST = ( DetectIRST and DetectIRST == true ) and Controller.Detection.IRST or nil - local DetectionRWR = ( DetectRWR and DetectRWR == true ) and Controller.Detection.RWR or nil - local DetectionDLINK = ( DetectDLINK and DetectDLINK == true ) and Controller.Detection.DLINK or nil - - self:T( { DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK } ) - - return self:_GetController():getDetectedTargets( DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK ) - end - - return nil -end - -function CONTROLLABLE:IsTargetDetected( DCSObject, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) - self:F2( self.ControllableName ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - - local DetectionVisual = ( DetectVisual and DetectVisual == true ) and Controller.Detection.VISUAL or nil - local DetectionOptical = ( DetectOptical and DetectOptical == true ) and Controller.Detection.OPTICAL or nil - local DetectionRadar = ( DetectRadar and DetectRadar == true ) and Controller.Detection.RADAR or nil - local DetectionIRST = ( DetectIRST and DetectIRST == true ) and Controller.Detection.IRST or nil - local DetectionRWR = ( DetectRWR and DetectRWR == true ) and Controller.Detection.RWR or nil - local DetectionDLINK = ( DetectDLINK and DetectDLINK == true ) and Controller.Detection.DLINK or nil - - local Controller = self:_GetController() - - local TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity - = Controller:isTargetDetected( DCSObject, DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK ) - - return TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity - end - - return nil -end - --- Options - ---- Can the CONTROLLABLE hold their weapons? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROEHoldFirePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Holding weapons. --- @param Wrapper.Controllable#CONTROLLABLE self --- @return Wrapper.Controllable#CONTROLLABLE self -function CONTROLLABLE:OptionROEHoldFire() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - elseif self:IsGround() then - Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.WEAPON_HOLD ) - elseif self:IsShip() then - Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.WEAPON_HOLD ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE attack returning on enemy fire? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROEReturnFirePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Return fire. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROEReturnFire() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.RETURN_FIRE ) - elseif self:IsGround() then - Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.RETURN_FIRE ) - elseif self:IsShip() then - Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.RETURN_FIRE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE attack designated targets? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROEOpenFirePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Openfire. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROEOpenFire() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - elseif self:IsGround() then - Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.OPEN_FIRE ) - elseif self:IsShip() then - Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.OPEN_FIRE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE attack targets of opportunity? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROEWeaponFreePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - ---- Weapon free. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROEWeaponFree() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE ignore enemy fire? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROTNoReactionPossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- No evasion on enemy threats. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROTNoReaction() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.NO_REACTION ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE evade using passive defenses? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROTPassiveDefensePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - ---- Evasion passive defense. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROTPassiveDefense() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.PASSIVE_DEFENCE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE evade on enemy fire? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROTEvadeFirePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- Evade on fire. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROTEvadeFire() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE evade on fire using vertical manoeuvres? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROTVerticalPossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- Evade on fire using vertical manoeuvres. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROTVertical() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) - end - - return self - end - - return nil -end - - ---- Set RTB on bingo fuel. --- @param #CONTROLLABLE self --- @param #boolean RTB true if RTB on bingo fuel (default), false if no RTB on bingo fuel. --- Warning! When you switch this option off, the airborne group will continue to fly until all fuel has been consumed, and will crash. --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionRTBBingoFuel( RTB ) --R2.2 - self:F2( { self.ControllableName } ) - - RTB = RTB or true - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.RTB_ON_BINGO, RTB ) - end - - return self - end - - return nil -end - - ---- Set RTB on ammo. --- @param #CONTROLLABLE self --- @param #boolean WeaponsFlag Weapons.flag enumerator. --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionRTBAmmo( WeaponsFlag ) - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.GROUND.id.RTB_ON_OUT_OF_AMMO, WeaponsFlag ) - end - - return self - end - - return nil -end - - - - - ---- Retrieve the controllable mission and allow to place function hooks within the mission waypoint plan. --- Use the method @{Controllable#CONTROLLABLE:WayPointFunction} to define the hook functions for specific waypoints. --- Use the method @{Controllable@CONTROLLABLE:WayPointExecute) to start the execution of the new mission plan. --- Note that when WayPointInitialize is called, the Mission of the controllable is RESTARTED! --- @param #CONTROLLABLE self --- @param #table WayPoints If WayPoints is given, then use the route. --- @return #CONTROLLABLE -function CONTROLLABLE:WayPointInitialize( WayPoints ) - self:F( { WayPoints } ) - - if WayPoints then - self.WayPoints = WayPoints - else - self.WayPoints = self:GetTaskRoute() - end - - return self -end - ---- Get the current WayPoints set with the WayPoint functions( Note that the WayPoints can be nil, although there ARE waypoints). --- @param #CONTROLLABLE self --- @return #table WayPoints If WayPoints is given, then return the WayPoints structure. -function CONTROLLABLE:GetWayPoints() - self:F( ) - - if self.WayPoints then - return self.WayPoints - end - - return nil -end - ---- Registers a waypoint function that will be executed when the controllable moves over the WayPoint. --- @param #CONTROLLABLE self --- @param #number WayPoint The waypoint number. Note that the start waypoint on the route is WayPoint 1! --- @param #number WayPointIndex When defining multiple WayPoint functions for one WayPoint, use WayPointIndex to set the sequence of actions. --- @param #function WayPointFunction The waypoint function to be called when the controllable moves over the waypoint. The waypoint function takes variable parameters. --- @return #CONTROLLABLE -function CONTROLLABLE:WayPointFunction( WayPoint, WayPointIndex, WayPointFunction, ... ) - self:F2( { WayPoint, WayPointIndex, WayPointFunction } ) - - table.insert( self.WayPoints[WayPoint].task.params.tasks, WayPointIndex ) - self.WayPoints[WayPoint].task.params.tasks[WayPointIndex] = self:TaskFunction( WayPointFunction, arg ) - return self -end - - ---- Executes the WayPoint plan. --- The function gets a WayPoint parameter, that you can use to restart the mission at a specific WayPoint. --- Note that when the WayPoint parameter is used, the new start mission waypoint of the controllable will be 1! --- @param #CONTROLLABLE self --- @param #number WayPoint The WayPoint from where to execute the mission. --- @param #number WaitTime The amount seconds to wait before initiating the mission. --- @return #CONTROLLABLE -function CONTROLLABLE:WayPointExecute( WayPoint, WaitTime ) - self:F( { WayPoint, WaitTime } ) - - if not WayPoint then - WayPoint = 1 - end - - -- When starting the mission from a certain point, the TaskPoints need to be deleted before the given WayPoint. - for TaskPointID = 1, WayPoint - 1 do - table.remove( self.WayPoints, 1 ) - end - - self:T3( self.WayPoints ) - - self:SetTask( self:TaskRoute( self.WayPoints ), WaitTime ) - - return self -end - ---- Returns if the Controllable contains AirPlanes. --- @param #CONTROLLABLE self --- @return #boolean true if Controllable contains AirPlanes. -function CONTROLLABLE:IsAirPlane() - self:F2() - - local DCSObject = self:GetDCSObject() - - if DCSObject then - local Category = DCSObject:getDesc().category - return Category == Unit.Category.AIRPLANE - end - - return nil -end - -function CONTROLLABLE:GetSize() - - local DCSObject = self:GetDCSObject() - - if DCSObject then - return 1 - else - return 0 - end -end - - --- Message APIs--- **Wrapper** -- GROUP wraps the DCS Class Group objects. --- --- === --- --- The @{#GROUP} class is a wrapper class to handle the DCS Group objects: --- --- * Support all DCS Group APIs. --- * Enhance with Group specific APIs not in the DCS Group API set. --- * Handle local Group Controller. --- * Manage the "state" of the DCS Group. --- --- **IMPORTANT: ONE SHOULD NEVER SANATIZE these GROUP OBJECT REFERENCES! (make the GROUP object references nil).** --- --- See the detailed documentation on the GROUP class. --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- * [**Entropy**](https://forums.eagle.ru/member.php?u=111471), **Afinegan**: Came up with the requirement for AIOnOff(). --- --- ==== --- --- @module Group - - ---- @type GROUP --- @extends Wrapper.Controllable#CONTROLLABLE --- @field #string GroupName The name of the group. - - ---- --- # GROUP class, extends @{Controllable#CONTROLLABLE} --- --- For each DCS Group object alive within a running mission, a GROUP wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Group objects are spawned (using the @{SPAWN} class). --- --- The GROUP class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference --- using the DCS Group or the DCS GroupName. --- --- Another thing to know is that GROUP objects do not "contain" the DCS Group object. --- The GROUP methods will reference the DCS Group object by name when it is needed during API execution. --- If the DCS Group object does not exist or is nil, the GROUP methods will return nil and log an exception in the DCS.log file. --- --- The GROUP class provides the following functions to retrieve quickly the relevant GROUP instance: --- --- * @{#GROUP.Find}(): Find a GROUP instance from the _DATABASE object using a DCS Group object. --- * @{#GROUP.FindByName}(): Find a GROUP instance from the _DATABASE object using a DCS Group name. --- --- ## GROUP task methods --- --- A GROUP is a @{Controllable}. See the @{Controllable} task methods section for a description of the task methods. --- --- ### Obtain the mission from group templates --- --- Group templates contain complete mission descriptions. Sometimes you want to copy a complete mission from a group and assign it to another: --- --- * @{Controllable#CONTROLLABLE.TaskMission}: (AIR + GROUND) Return a mission task from a mission template. --- --- ## GROUP Command methods --- --- A GROUP is a @{Controllable}. See the @{Controllable} command methods section for a description of the command methods. --- --- ## GROUP option methods --- --- A GROUP is a @{Controllable}. See the @{Controllable} option methods section for a description of the option methods. --- --- ## GROUP Zone validation methods --- --- The group can be validated whether it is completely, partly or not within a @{Zone}. --- Use the following Zone validation methods on the group: --- --- * @{#GROUP.IsCompletelyInZone}: Returns true if all units of the group are within a @{Zone}. --- * @{#GROUP.IsPartlyInZone}: Returns true if some units of the group are within a @{Zone}. --- * @{#GROUP.IsNotInZone}: Returns true if none of the group units of the group are within a @{Zone}. --- --- The zone can be of any @{Zone} class derived from @{Zone#ZONE_BASE}. So, these methods are polymorphic to the zones tested on. --- --- ## GROUP AI methods --- --- A GROUP has AI methods to control the AI activation. --- --- * @{#GROUP.SetAIOnOff}(): Turns the GROUP AI On or Off. --- * @{#GROUP.SetAIOn}(): Turns the GROUP AI On. --- * @{#GROUP.SetAIOff}(): Turns the GROUP AI Off. --- --- @field #GROUP GROUP -GROUP = { - ClassName = "GROUP", -} - - ---- Enumerator for location at airbases --- @type GROUP.Takeoff -GROUP.Takeoff = { - Air = 1, - Runway = 2, - Hot = 3, - Cold = 4, -} - -GROUPTEMPLATE = {} - -GROUPTEMPLATE.Takeoff = { - [GROUP.Takeoff.Air] = { "Turning Point", "Turning Point" }, - [GROUP.Takeoff.Runway] = { "TakeOff", "From Runway" }, - [GROUP.Takeoff.Hot] = { "TakeOffParkingHot", "From Parking Area Hot" }, - [GROUP.Takeoff.Cold] = { "TakeOffParking", "From Parking Area" } -} - ---- Create a new GROUP from a DCSGroup --- @param #GROUP self --- @param Dcs.DCSWrapper.Group#Group GroupName The DCS Group name --- @return #GROUP self -function GROUP:Register( GroupName ) - self = BASE:Inherit( self, CONTROLLABLE:New( GroupName ) ) - self:F2( GroupName ) - self.GroupName = GroupName - - self:SetEventPriority( 4 ) - return self -end - --- Reference methods. - ---- Find the GROUP wrapper class instance using the DCS Group. --- @param #GROUP self --- @param Dcs.DCSWrapper.Group#Group DCSGroup The DCS Group. --- @return #GROUP The GROUP. -function GROUP:Find( DCSGroup ) - - local GroupName = DCSGroup:getName() -- Wrapper.Group#GROUP - local GroupFound = _DATABASE:FindGroup( GroupName ) - return GroupFound -end - ---- Find the created GROUP using the DCS Group Name. --- @param #GROUP self --- @param #string GroupName The DCS Group Name. --- @return #GROUP The GROUP. -function GROUP:FindByName( GroupName ) - - local GroupFound = _DATABASE:FindGroup( GroupName ) - return GroupFound -end - --- DCS Group methods support. - ---- Returns the DCS Group. --- @param #GROUP self --- @return Dcs.DCSWrapper.Group#Group The DCS Group. -function GROUP:GetDCSObject() - local DCSGroup = Group.getByName( self.GroupName ) - - if DCSGroup then - return DCSGroup - end - - return nil -end - ---- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Position The 3D position vectors of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function GROUP:GetPositionVec3() -- Overridden from POSITIONABLE:GetPositionVec3() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePosition = DCSPositionable:getUnits()[1]:getPosition().p - self:T3( PositionablePosition ) - return PositionablePosition - end - - return nil -end - ---- Returns if the Group is alive. --- The Group must: --- --- * Exist at run-time. --- * Has at least one unit. --- --- When the first @{Unit} of the Group is active, it will return true. --- If the first @{Unit} of the Group is inactive, it will return false. --- --- @param #GROUP self --- @return #boolean true if the Group is alive and active. --- @return #boolean false if the Group is alive but inactive. --- @return #nil if the group does not exist anymore. -function GROUP:IsAlive() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() -- Dcs.DCSGroup#Group - - if DCSGroup then - if DCSGroup:isExist() then - local DCSUnit = DCSGroup:getUnit(1) -- Dcs.DCSUnit#Unit - if DCSUnit then - local GroupIsAlive = DCSUnit:isActive() - self:T3( GroupIsAlive ) - return GroupIsAlive - end - end - end - - return nil -end - ---- Destroys the DCS Group and all of its DCS Units. --- Note that this destroy method also raises a destroy event at run-time. --- So all event listeners will catch the destroy event of this DCS Group. --- @param #GROUP self -function GROUP:Destroy() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - self:CreateEventCrash( timer.getTime(), UnitData ) - end - DCSGroup:destroy() - DCSGroup = nil - end - - return nil -end - ---- Returns category of the DCS Group. --- @param #GROUP self --- @return Dcs.DCSWrapper.Group#Group.Category The category ID -function GROUP:GetCategory() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T3( GroupCategory ) - return GroupCategory - end - - return nil -end - ---- Returns the category name of the #GROUP. --- @param #GROUP self --- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship -function GROUP:GetCategoryName() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local CategoryNames = { - [Group.Category.AIRPLANE] = "Airplane", - [Group.Category.HELICOPTER] = "Helicopter", - [Group.Category.GROUND] = "Ground Unit", - [Group.Category.SHIP] = "Ship", - } - local GroupCategory = DCSGroup:getCategory() - self:T3( GroupCategory ) - - return CategoryNames[GroupCategory] - end - - return nil -end - - ---- Returns the coalition of the DCS Group. --- @param #GROUP self --- @return Dcs.DCSCoalitionWrapper.Object#coalition.side The coalition side of the DCS Group. -function GROUP:GetCoalition() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local GroupCoalition = DCSGroup:getCoalition() - self:T3( GroupCoalition ) - return GroupCoalition - end - - return nil -end - ---- Returns the country of the DCS Group. --- @param #GROUP self --- @return Dcs.DCScountry#country.id The country identifier. --- @return #nil The DCS Group is not existing or alive. -function GROUP:GetCountry() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local GroupCountry = DCSGroup:getUnit(1):getCountry() - self:T3( GroupCountry ) - return GroupCountry - end - - return nil -end - ---- Returns the UNIT wrapper class with number UnitNumber. --- If the underlying DCS Unit does not exist, the method will return nil. . --- @param #GROUP self --- @param #number UnitNumber The number of the UNIT wrapper class to be returned. --- @return Wrapper.Unit#UNIT The UNIT wrapper class. -function GROUP:GetUnit( UnitNumber ) - self:F2( { self.GroupName, UnitNumber } ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local DCSUnit = DCSGroup:getUnit( UnitNumber ) - local UnitFound = UNIT:Find( DCSGroup:getUnit( UnitNumber ) ) - self:T2( UnitFound ) - return UnitFound - end - - return nil -end - ---- Returns the DCS Unit with number UnitNumber. --- If the underlying DCS Unit does not exist, the method will return nil. . --- @param #GROUP self --- @param #number UnitNumber The number of the DCS Unit to be returned. --- @return Dcs.DCSWrapper.Unit#Unit The DCS Unit. -function GROUP:GetDCSUnit( UnitNumber ) - self:F2( { self.GroupName, UnitNumber } ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local DCSUnitFound = DCSGroup:getUnit( UnitNumber ) - self:T3( DCSUnitFound ) - return DCSUnitFound - end - - return nil -end - ---- Returns current size of the DCS Group. --- If some of the DCS Units of the DCS Group are destroyed the size of the DCS Group is changed. --- @param #GROUP self --- @return #number The DCS Group size. -function GROUP:GetSize() - self:F2( { self.GroupName } ) - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupSize = DCSGroup:getSize() - - if GroupSize then - self:T3( GroupSize ) - return GroupSize - else - return 0 - end - end - - return nil -end - ---- ---- Returns the initial size of the DCS Group. --- If some of the DCS Units of the DCS Group are destroyed, the initial size of the DCS Group is unchanged. --- @param #GROUP self --- @return #number The DCS Group initial size. -function GROUP:GetInitialSize() - self:F2( { self.GroupName } ) - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupInitialSize = DCSGroup:getInitialSize() - self:T3( GroupInitialSize ) - return GroupInitialSize - end - - return nil -end - - ---- Returns the DCS Units of the DCS Group. --- @param #GROUP self --- @return #table The DCS Units. -function GROUP:GetDCSUnits() - self:F2( { self.GroupName } ) - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local DCSUnits = DCSGroup:getUnits() - self:T3( DCSUnits ) - return DCSUnits - end - - return nil -end - - ---- Activates a GROUP. --- @param #GROUP self -function GROUP:Activate() - self:F2( { self.GroupName } ) - trigger.action.activateGroup( self:GetDCSObject() ) - return self:GetDCSObject() -end - - ---- Gets the type name of the group. --- @param #GROUP self --- @return #string The type name of the group. -function GROUP:GetTypeName() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupTypeName = DCSGroup:getUnit(1):getTypeName() - self:T3( GroupTypeName ) - return( GroupTypeName ) - end - - return nil -end - ---- Gets the player name of the group. --- @param #GROUP self --- @return #string The player name of the group. -function GROUP:GetPlayerName() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local PlayerName = DCSGroup:getUnit(1):getPlayerName() - self:T3( PlayerName ) - return( PlayerName ) - end - - return nil -end - - ---- Gets the CallSign of the first DCS Unit of the DCS Group. --- @param #GROUP self --- @return #string The CallSign of the first DCS Unit of the DCS Group. -function GROUP:GetCallsign() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCallSign = DCSGroup:getUnit(1):getCallsign() - self:T3( GroupCallSign ) - return GroupCallSign - end - - return nil -end - ---- Returns the current point (Vec2 vector) of the first DCS Unit in the DCS Group. --- @param #GROUP self --- @return Dcs.DCSTypes#Vec2 Current Vec2 point of the first DCS Unit of the DCS Group. -function GROUP:GetVec2() - self:F2( self.GroupName ) - - local UnitPoint = self:GetUnit(1) - UnitPoint:GetVec2() - local GroupPointVec2 = UnitPoint:GetVec2() - self:T3( GroupPointVec2 ) - return GroupPointVec2 -end - ---- Returns the current Vec3 vector of the first DCS Unit in the GROUP. --- @param #GROUP self --- @return Dcs.DCSTypes#Vec3 Current Vec3 of the first DCS Unit of the GROUP. -function GROUP:GetVec3() - self:F2( self.GroupName ) - - local GroupVec3 = self:GetUnit(1):GetVec3() - self:T3( GroupVec3 ) - return GroupVec3 -end - ---- Returns a POINT_VEC2 object indicating the point in 2D of the first UNIT of the GROUP within the mission. --- @param #GROUP self --- @return Core.Point#POINT_VEC2 The 2D point vector of the first DCS Unit of the GROUP. --- @return #nil The first UNIT is not existing or alive. -function GROUP:GetPointVec2() - self:F2(self.GroupName) - - local FirstUnit = self:GetUnit(1) - - if FirstUnit then - local FirstUnitPointVec2 = FirstUnit:GetPointVec2() - self:T3(FirstUnitPointVec2) - return FirstUnitPointVec2 - end - - return nil -end - ---- Returns a COORDINATE object indicating the point of the first UNIT of the GROUP within the mission. --- @param Wrapper.Group#GROUP self --- @return Core.Point#COORDINATE The COORDINATE of the GROUP. -function GROUP:GetCoordinate() - self:F2( self.PositionableName ) - - local FirstUnit = self:GetUnit(1) - - if FirstUnit then - local FirstUnitCoordinate = FirstUnit:GetCoordinate() - self:T3(FirstUnitCoordinate) - return FirstUnitCoordinate - end - - return nil -end - - ---- Returns a random @{DCSTypes#Vec3} vector (point in 3D of the UNIT within the mission) within a range around the first UNIT of the GROUP. --- @param #GROUP self --- @param #number Radius --- @return Dcs.DCSTypes#Vec3 The random 3D point vector around the first UNIT of the GROUP. --- @return #nil The GROUP is invalid or empty --- @usage --- -- If Radius is ignored, returns the Dcs.DCSTypes#Vec3 of first UNIT of the GROUP -function GROUP:GetRandomVec3(Radius) - self:F2(self.GroupName) - - local FirstUnit = self:GetUnit(1) - - if FirstUnit then - local FirstUnitRandomPointVec3 = FirstUnit:GetRandomVec3(Radius) - self:T3(FirstUnitRandomPointVec3) - return FirstUnitRandomPointVec3 - end - - return nil -end - ---- Returns the mean heading of every UNIT in the GROUP in degrees --- @param #GROUP self --- @return #number mean heading of the GROUP --- @return #nil The first UNIT is not existing or alive. -function GROUP:GetHeading() - self:F2(self.GroupName) - - local GroupSize = self:GetSize() - local HeadingAccumulator = 0 - - if GroupSize then - for i = 1, GroupSize do - HeadingAccumulator = HeadingAccumulator + self:GetUnit(i):GetHeading() - end - return math.floor(HeadingAccumulator / GroupSize) - end - - return nil - -end - ---- Returns relative amount of fuel (from 0.0 to 1.0) the group has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. --- @param #GROUP self --- @return #number The relative amount of fuel (from 0.0 to 1.0). --- @return #nil The GROUP is not existing or alive. -function GROUP:GetFuel() - self:F( self.ControllableName ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local GroupSize = self:GetSize() - local TotalFuel = 0 - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Wrapper.Unit#UNIT - local UnitFuel = Unit:GetFuel() - self:F( { Fuel = UnitFuel } ) - TotalFuel = TotalFuel + UnitFuel - end - local GroupFuel = TotalFuel / GroupSize - return GroupFuel - end - - return 0 -end - - -do -- Is Zone methods - ---- Returns true if all units of the group are within a @{Zone}. --- @param #GROUP self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} -function GROUP:IsCompletelyInZone( Zone ) - self:F2( { self.GroupName, Zone } ) - - if not self:IsAlive() then return false end - - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Wrapper.Unit#UNIT - if Zone:IsVec3InZone( Unit:GetVec3() ) then - else - return false - end - end - - return true -end - ---- Returns true if some units of the group are within a @{Zone}. --- @param #GROUP self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is partially within the @{Zone#ZONE_BASE} -function GROUP:IsPartlyInZone( Zone ) - self:F2( { self.GroupName, Zone } ) - - local IsOneUnitInZone = false - local IsOneUnitOutsideZone = false - - if not self:IsAlive() then return false end - - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Wrapper.Unit#UNIT - if Zone:IsVec3InZone( Unit:GetVec3() ) then - IsOneUnitInZone = true - else - IsOneUnitOutsideZone = true - end - end - - if IsOneUnitInZone and IsOneUnitOutsideZone then - return true - else - return false - end -end - ---- Returns true if none of the group units of the group are within a @{Zone}. --- @param #GROUP self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is not within the @{Zone#ZONE_BASE} -function GROUP:IsNotInZone( Zone ) - self:F2( { self.GroupName, Zone } ) - - if not self:IsAlive() then return true end - - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Wrapper.Unit#UNIT - if Zone:IsVec3InZone( Unit:GetVec3() ) then - return false - end - end - - return true -end - ---- Returns the number of UNITs that are in the @{Zone} --- @param #GROUP self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #number The number of UNITs that are in the @{Zone} -function GROUP:CountInZone( Zone ) - self:F2( {self.GroupName, Zone} ) - local Count = 0 - - if not self:IsAlive() then return Count end - - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Wrapper.Unit#UNIT - if Zone:IsVec3InZone( Unit:GetVec3() ) then - Count = Count + 1 - end - end - - return Count -end - ---- Returns if the group is of an air category. --- If the group is a helicopter or a plane, then this method will return true, otherwise false. --- @param #GROUP self --- @return #boolean Air category evaluation result. -function GROUP:IsAir() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local IsAirResult = DCSGroup:getCategory() == Group.Category.AIRPLANE or DCSGroup:getCategory() == Group.Category.HELICOPTER - self:T3( IsAirResult ) - return IsAirResult - end - - return nil -end - ---- Returns if the DCS Group contains Helicopters. --- @param #GROUP self --- @return #boolean true if DCS Group contains Helicopters. -function GROUP:IsHelicopter() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T2( GroupCategory ) - return GroupCategory == Group.Category.HELICOPTER - end - - return nil -end - ---- Returns if the DCS Group contains AirPlanes. --- @param #GROUP self --- @return #boolean true if DCS Group contains AirPlanes. -function GROUP:IsAirPlane() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T2( GroupCategory ) - return GroupCategory == Group.Category.AIRPLANE - end - - return nil -end - ---- Returns if the DCS Group contains Ground troops. --- @param #GROUP self --- @return #boolean true if DCS Group contains Ground troops. -function GROUP:IsGround() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T2( GroupCategory ) - return GroupCategory == Group.Category.GROUND - end - - return nil -end - ---- Returns if the DCS Group contains Ships. --- @param #GROUP self --- @return #boolean true if DCS Group contains Ships. -function GROUP:IsShip() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T2( GroupCategory ) - return GroupCategory == Group.Category.SHIP - end - - return nil -end - ---- Returns if all units of the group are on the ground or landed. --- If all units of this group are on the ground, this function will return true, otherwise false. --- @param #GROUP self --- @return #boolean All units on the ground result. -function GROUP:AllOnGround() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local AllOnGroundResult = true - - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - if UnitData:inAir() then - AllOnGroundResult = false - end - end - - self:T3( AllOnGroundResult ) - return AllOnGroundResult - end - - return nil -end - -end - -do -- AI methods - - --- Turns the AI On or Off for the GROUP. - -- @param #GROUP self - -- @param #boolean AIOnOff The value true turns the AI On, the value false turns the AI Off. - -- @return #GROUP The GROUP. - function GROUP:SetAIOnOff( AIOnOff ) - - local DCSGroup = self:GetDCSObject() -- Dcs.DCSGroup#Group - - if DCSGroup then - local DCSController = DCSGroup:getController() -- Dcs.DCSController#Controller - if DCSController then - DCSController:setOnOff( AIOnOff ) - return self - end - end - - return nil - end - - --- Turns the AI On for the GROUP. - -- @param #GROUP self - -- @return #GROUP The GROUP. - function GROUP:SetAIOn() - - return self:SetAIOnOff( true ) - end - - --- Turns the AI Off for the GROUP. - -- @param #GROUP self - -- @return #GROUP The GROUP. - function GROUP:SetAIOff() - - return self:SetAIOnOff( false ) - end - -end - - - ---- Returns the current maximum velocity of the group. --- Each unit within the group gets evaluated, and the maximum velocity (= the unit which is going the fastest) is returned. --- @param #GROUP self --- @return #number Maximum velocity found. -function GROUP:GetMaxVelocity() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupVelocityMax = 0 - - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - - local UnitVelocityVec3 = UnitData:getVelocity() - local UnitVelocity = math.abs( UnitVelocityVec3.x ) + math.abs( UnitVelocityVec3.y ) + math.abs( UnitVelocityVec3.z ) - - if UnitVelocity > GroupVelocityMax then - GroupVelocityMax = UnitVelocity - end - end - - return GroupVelocityMax - end - - return nil -end - ---- Returns the current minimum height of the group. --- Each unit within the group gets evaluated, and the minimum height (= the unit which is the lowest elevated) is returned. --- @param #GROUP self --- @return #number Minimum height found. -function GROUP:GetMinHeight() - self:F2() - -end - ---- Returns the current maximum height of the group. --- Each unit within the group gets evaluated, and the maximum height (= the unit which is the highest elevated) is returned. --- @param #GROUP self --- @return #number Maximum height found. -function GROUP:GetMaxHeight() - self:F2() - -end - --- SPAWNING - ---- Respawn the @{GROUP} using a (tweaked) template of the Group. --- The template must be retrieved with the @{Group#GROUP.GetTemplate}() function. --- The template contains all the definitions as declared within the mission file. --- To understand templates, do the following: --- --- * unpack your .miz file into a directory using 7-zip. --- * browse in the directory created to the file **mission**. --- * open the file and search for the country group definitions. --- --- Your group template will contain the fields as described within the mission file. --- --- This function will: --- --- * Get the current position and heading of the group. --- * When the group is alive, it will tweak the template x, y and heading coordinates of the group and the embedded units to the current units positions. --- * Then it will destroy the current alive group. --- * And it will respawn the group using your new template definition. --- @param Wrapper.Group#GROUP self --- @param #table Template The template of the Group retrieved with GROUP:GetTemplate() -function GROUP:Respawn( Template ) - - if self:IsAlive() then - local Vec3 = self:GetVec3() - Template.x = Vec3.x - Template.y = Vec3.z - --Template.x = nil - --Template.y = nil - - self:E( #Template.units ) - for UnitID, UnitData in pairs( self:GetUnits() ) do - local GroupUnit = UnitData -- Wrapper.Unit#UNIT - self:E( GroupUnit:GetName() ) - if GroupUnit:IsAlive() then - local GroupUnitVec3 = GroupUnit:GetVec3() - local GroupUnitHeading = GroupUnit:GetHeading() - Template.units[UnitID].alt = GroupUnitVec3.y - Template.units[UnitID].x = GroupUnitVec3.x - Template.units[UnitID].y = GroupUnitVec3.z - Template.units[UnitID].heading = GroupUnitHeading - self:E( { UnitID, Template.units[UnitID], Template.units[UnitID] } ) - end - end - - end - - self:Destroy() - _DATABASE:Spawn( Template ) - - self:ResetEvents() - -end - ---- Returns the group template from the @{DATABASE} (_DATABASE object). --- @param #GROUP self --- @return #table -function GROUP:GetTemplate() - local GroupName = self:GetName() - return UTILS.DeepCopy( _DATABASE:GetGroupTemplate( GroupName ) ) -end - ---- Returns the group template route.points[] (the waypoints) from the @{DATABASE} (_DATABASE object). --- @param #GROUP self --- @return #table -function GROUP:GetTemplateRoutePoints() - local GroupName = self:GetName() - return UTILS.DeepCopy( _DATABASE:GetGroupTemplate( GroupName ).route.points ) -end - - - ---- Sets the controlled status in a Template. --- @param #GROUP self --- @param #boolean Controlled true is controlled, false is uncontrolled. --- @return #table -function GROUP:SetTemplateControlled( Template, Controlled ) - Template.uncontrolled = not Controlled - return Template -end - ---- Sets the CountryID of the group in a Template. --- @param #GROUP self --- @param Dcs.DCScountry#country.id CountryID The country ID. --- @return #table -function GROUP:SetTemplateCountry( Template, CountryID ) - Template.CountryID = CountryID - return Template -end - ---- Sets the CoalitionID of the group in a Template. --- @param #GROUP self --- @param Dcs.DCSCoalitionWrapper.Object#coalition.side CoalitionID The coalition ID. --- @return #table -function GROUP:SetTemplateCoalition( Template, CoalitionID ) - Template.CoalitionID = CoalitionID - return Template -end - - - - ---- Return the mission template of the group. --- @param #GROUP self --- @return #table The MissionTemplate -function GROUP:GetTaskMission() - self:F2( self.GroupName ) - - return routines.utils.deepCopy( _DATABASE.Templates.Groups[self.GroupName].Template ) -end - ---- Return the mission route of the group. --- @param #GROUP self --- @return #table The mission route defined by points. -function GROUP:GetTaskRoute() - self:F2( self.GroupName ) - - return routines.utils.deepCopy( _DATABASE.Templates.Groups[self.GroupName].Template.route.points ) -end - ---- Return the route of a group by using the @{Database#DATABASE} class. --- @param #GROUP self --- @param #number Begin The route point from where the copy will start. The base route point is 0. --- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0. --- @param #boolean Randomize Randomization of the route, when true. --- @param #number Radius When randomization is on, the randomization is within the radius. -function GROUP:CopyRoute( Begin, End, Randomize, Radius ) - self:F2( { Begin, End } ) - - local Points = {} - - -- Could be a Spawned Group - local GroupName = string.match( self:GetName(), ".*#" ) - if GroupName then - GroupName = GroupName:sub( 1, -2 ) - else - GroupName = self:GetName() - end - - self:T3( { GroupName } ) - - local Template = _DATABASE.Templates.Groups[GroupName].Template - - if Template then - if not Begin then - Begin = 0 - end - if not End then - End = 0 - end - - for TPointID = Begin + 1, #Template.route.points - End do - if Template.route.points[TPointID] then - Points[#Points+1] = routines.utils.deepCopy( Template.route.points[TPointID] ) - if Randomize then - if not Radius then - Radius = 500 - end - Points[#Points].x = Points[#Points].x + math.random( Radius * -1, Radius ) - Points[#Points].y = Points[#Points].y + math.random( Radius * -1, Radius ) - end - end - end - return Points - else - error( "Template not found for Group : " .. GroupName ) - end - - return nil -end - ---- Calculate the maxium A2G threat level of the Group. --- @param #GROUP self -function GROUP:CalculateThreatLevelA2G() - - local MaxThreatLevelA2G = 0 - for UnitName, UnitData in pairs( self:GetUnits() ) do - local ThreatUnit = UnitData -- Wrapper.Unit#UNIT - local ThreatLevelA2G = ThreatUnit:GetThreatLevel() - if ThreatLevelA2G > MaxThreatLevelA2G then - MaxThreatLevelA2G = ThreatLevelA2G - end - end - - self:T3( MaxThreatLevelA2G ) - return MaxThreatLevelA2G -end - ---- Returns true if the first unit of the GROUP is in the air. --- @param Wrapper.Group#GROUP self --- @return #boolean true if in the first unit of the group is in the air. --- @return #nil The GROUP is not existing or not alive. -function GROUP:InAir() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local DCSUnit = DCSGroup:getUnit(1) - if DCSUnit then - local GroupInAir = DCSGroup:getUnit(1):inAir() - self:T3( GroupInAir ) - return GroupInAir - end - end - - return nil -end - -do -- Route methods - - --- (AIR) Return the Group to an @{Airbase#AIRBASE}. - -- The following things are to be taken into account: - -- - -- * The group is respawned to achieve the RTB, there may be side artefacts as a result of this. (Like weapons suddenly come back). - -- * A group consisting out of more than one unit, may rejoin formation when respawned. - -- * A speed can be given in km/h. If no speed is specified, the maximum speed of the first unit will be taken to return to base. - -- * When there is no @{Airbase} object specified, the group will return to the home base if the route of the group is pinned at take-off or at landing to a base. - -- * When there is no @{Airbase} object specified and the group route is not pinned to any airbase, it will return to the nearest airbase. - -- - -- @param #GROUP self - -- @param Wrapper.Airbase#AIRBASE RTBAirbase (optional) The @{Airbase} to return to. If blank, the controllable will return to the nearest friendly airbase. - -- @param #number Speed (optional) The Speed, if no Speed is given, the maximum Speed of the first unit is selected. - -- @return #GROUP - function GROUP:RouteRTB( RTBAirbase, Speed ) - self:F2( { RTBAirbase, Speed } ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - - if RTBAirbase then - - local GroupPoint = self:GetVec2() - local GroupVelocity = self:GetUnit(1):GetDesc().speedMax - - local PointFrom = {} - PointFrom.x = GroupPoint.x - PointFrom.y = GroupPoint.y - PointFrom.type = "Turning Point" - PointFrom.action = "Turning Point" - PointFrom.speed = GroupVelocity - - - local PointTo = {} - local AirbasePointVec2 = RTBAirbase:GetPointVec2() - local AirbaseAirPoint = AirbasePointVec2:WaypointAir( - POINT_VEC3.RoutePointAltType.BARO, - "Land", - "Landing", - Speed or self:GetUnit(1):GetDesc().speedMax - ) - - AirbaseAirPoint["airdromeId"] = RTBAirbase:GetID() - AirbaseAirPoint["speed_locked"] = true, - - self:E(AirbaseAirPoint ) - - local Points = { PointFrom, AirbaseAirPoint } - - self:T3( Points ) - - local Template = self:GetTemplate() - Template.route.points = Points - self:Respawn( Template ) - - self:Route( Points ) - - self:Respawn(Template) - else - self:ClearTasks() - end - end - - return self - end - -end - -function GROUP:OnReSpawn( ReSpawnFunction ) - - self.ReSpawnFunction = ReSpawnFunction -end - -do -- Event Handling - - --- Subscribe to a DCS Event. - -- @param #GROUP self - -- @param Core.Event#EVENTS Event - -- @param #function EventFunction (optional) The function to be called when the event occurs for the GROUP. - -- @return #GROUP - function GROUP:HandleEvent( Event, EventFunction, ... ) - - self:EventDispatcher():OnEventForGroup( self:GetName(), EventFunction, self, Event, ... ) - - return self - end - - --- UnSubscribe to a DCS event. - -- @param #GROUP self - -- @param Core.Event#EVENTS Event - -- @return #GROUP - function GROUP:UnHandleEvent( Event ) - - self:EventDispatcher():RemoveEvent( self, Event ) - - return self - end - - --- Reset the subscriptions. - -- @param #GROUP self - -- @return #GROUP - function GROUP:ResetEvents() - - self:EventDispatcher():Reset( self ) - - for UnitID, UnitData in pairs( self:GetUnits() ) do - UnitData:ResetEvents() - end - - return self - end - -end - -do -- Players - - --- Get player names - -- @param #GROUP self - -- @return #table The group has players, an array of player names is returned. - -- @return #nil The group has no players - function GROUP:GetPlayerNames() - - local PlayerNames = {} - - local Units = self:GetUnits() - for UnitID, UnitData in pairs( Units ) do - local Unit = UnitData -- Wrapper.Unit#UNIT - local PlayerName = Unit:GetPlayerName() - if PlayerName and PlayerName ~= "" then - PlayerNames = PlayerNames or {} - table.insert( PlayerNames, PlayerName ) - end - end - - self:F2( PlayerNames ) - return PlayerNames - end - -end - ---do -- Smoke --- ------ Signal a flare at the position of the GROUP. ----- @param #GROUP self ----- @param Utilities.Utils#FLARECOLOR FlareColor ---function GROUP:Flare( FlareColor ) --- self:F2() --- trigger.action.signalFlare( self:GetVec3(), FlareColor , 0 ) ---end --- ------ Signal a white flare at the position of the GROUP. ----- @param #GROUP self ---function GROUP:FlareWhite() --- self:F2() --- trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.White , 0 ) ---end --- ------ Signal a yellow flare at the position of the GROUP. ----- @param #GROUP self ---function GROUP:FlareYellow() --- self:F2() --- trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Yellow , 0 ) ---end --- ------ Signal a green flare at the position of the GROUP. ----- @param #GROUP self ---function GROUP:FlareGreen() --- self:F2() --- trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Green , 0 ) ---end --- ------ Signal a red flare at the position of the GROUP. ----- @param #GROUP self ---function GROUP:FlareRed() --- self:F2() --- local Vec3 = self:GetVec3() --- if Vec3 then --- trigger.action.signalFlare( Vec3, trigger.flareColor.Red, 0 ) --- end ---end --- ------ Smoke the GROUP. ----- @param #GROUP self ---function GROUP:Smoke( SmokeColor, Range ) --- self:F2() --- if Range then --- trigger.action.smoke( self:GetRandomVec3( Range ), SmokeColor ) --- else --- trigger.action.smoke( self:GetVec3(), SmokeColor ) --- end --- ---end --- ------ Smoke the GROUP Green. ----- @param #GROUP self ---function GROUP:SmokeGreen() --- self:F2() --- trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Green ) ---end --- ------ Smoke the GROUP Red. ----- @param #GROUP self ---function GROUP:SmokeRed() --- self:F2() --- trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Red ) ---end --- ------ Smoke the GROUP White. ----- @param #GROUP self ---function GROUP:SmokeWhite() --- self:F2() --- trigger.action.smoke( self:GetVec3(), trigger.smokeColor.White ) ---end --- ------ Smoke the GROUP Orange. ----- @param #GROUP self ---function GROUP:SmokeOrange() --- self:F2() --- trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Orange ) ---end --- ------ Smoke the GROUP Blue. ----- @param #GROUP self ---function GROUP:SmokeBlue() --- self:F2() --- trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Blue ) ---end --- --- --- ---end--- **Wrapper** - UNIT is a wrapper class for the DCS Class Unit. --- --- === --- --- The @{#UNIT} class is a wrapper class to handle the DCS Unit objects: --- --- * Support all DCS Unit APIs. --- * Enhance with Unit specific APIs not in the DCS Unit API set. --- * Handle local Unit Controller. --- * Manage the "state" of the DCS Unit. --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- ==== --- --- @module Unit - - ---- @type UNIT --- @extends Wrapper.Controllable#CONTROLLABLE - ---- --- # UNIT class, extends @{Controllable#CONTROLLABLE} --- --- For each DCS Unit object alive within a running mission, a UNIT wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Unit objects are spawned (using the @{SPAWN} class). --- --- The UNIT class **does not contain a :New()** method, rather it provides **:Find()** methods to retrieve the object reference --- using the DCS Unit or the DCS UnitName. --- --- Another thing to know is that UNIT objects do not "contain" the DCS Unit object. --- The UNIT methods will reference the DCS Unit object by name when it is needed during API execution. --- If the DCS Unit object does not exist or is nil, the UNIT methods will return nil and log an exception in the DCS.log file. --- --- The UNIT class provides the following functions to retrieve quickly the relevant UNIT instance: --- --- * @{#UNIT.Find}(): Find a UNIT instance from the _DATABASE object using a DCS Unit object. --- * @{#UNIT.FindByName}(): Find a UNIT instance from the _DATABASE object using a DCS Unit name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these UNIT OBJECT REFERENCES! (make the UNIT object references nil). --- --- ## DCS UNIT APIs --- --- The DCS Unit APIs are used extensively within MOOSE. The UNIT class has for each DCS Unit API a corresponding method. --- To be able to distinguish easily in your code the difference between a UNIT API call and a DCS Unit API call, --- the first letter of the method is also capitalized. So, by example, the DCS Unit method @{DCSWrapper.Unit#Unit.getName}() --- is implemented in the UNIT class as @{#UNIT.GetName}(). --- --- ## Smoke, Flare Units --- --- The UNIT class provides methods to smoke or flare units easily. --- The @{#UNIT.SmokeBlue}(), @{#UNIT.SmokeGreen}(),@{#UNIT.SmokeOrange}(), @{#UNIT.SmokeRed}(), @{#UNIT.SmokeRed}() methods --- will smoke the unit in the corresponding color. Note that smoking a unit is done at the current position of the DCS Unit. --- When the DCS Unit moves for whatever reason, the smoking will still continue! --- The @{#UNIT.FlareGreen}(), @{#UNIT.FlareRed}(), @{#UNIT.FlareWhite}(), @{#UNIT.FlareYellow}() --- methods will fire off a flare in the air with the corresponding color. Note that a flare is a one-off shot and its effect is of very short duration. --- --- ## Location Position, Point --- --- The UNIT class provides methods to obtain the current point or position of the DCS Unit. --- The @{#UNIT.GetPointVec2}(), @{#UNIT.GetVec3}() will obtain the current **location** of the DCS Unit in a Vec2 (2D) or a **point** in a Vec3 (3D) vector respectively. --- If you want to obtain the complete **3D position** including ori�ntation and direction vectors, consult the @{#UNIT.GetPositionVec3}() method respectively. --- --- ## Test if alive --- --- The @{#UNIT.IsAlive}(), @{#UNIT.IsActive}() methods determines if the DCS Unit is alive, meaning, it is existing and active. --- --- ## Test for proximity --- --- The UNIT class contains methods to test the location or proximity against zones or other objects. --- --- ### Zones range --- --- To test whether the Unit is within a **zone**, use the @{#UNIT.IsInZone}() or the @{#UNIT.IsNotInZone}() methods. Any zone can be tested on, but the zone must be derived from @{Zone#ZONE_BASE}. --- --- ### Unit range --- --- * Test if another DCS Unit is within a given radius of the current DCS Unit, use the @{#UNIT.OtherUnitInRadius}() method. --- --- ## Test Line of Sight --- --- * Use the @{#UNIT.IsLOS}() method to check if the given unit is within line of sight. --- --- --- @field #UNIT UNIT -UNIT = { - ClassName="UNIT", -} - - ---- Unit.SensorType --- @type Unit.SensorType --- @field OPTIC --- @field RADAR --- @field IRST --- @field RWR - - --- Registration. - ---- Create a new UNIT from DCSUnit. --- @param #UNIT self --- @param #string UnitName The name of the DCS unit. --- @return #UNIT -function UNIT:Register( UnitName ) - local self = BASE:Inherit( self, CONTROLLABLE:New( UnitName ) ) - self.UnitName = UnitName - - self:SetEventPriority( 3 ) - return self -end - --- Reference methods. - ---- Finds a UNIT from the _DATABASE using a DCSUnit object. --- @param #UNIT self --- @param Dcs.DCSWrapper.Unit#Unit DCSUnit An existing DCS Unit object reference. --- @return #UNIT self -function UNIT:Find( DCSUnit ) - - local UnitName = DCSUnit:getName() - local UnitFound = _DATABASE:FindUnit( UnitName ) - return UnitFound -end - ---- Find a UNIT in the _DATABASE using the name of an existing DCS Unit. --- @param #UNIT self --- @param #string UnitName The Unit Name. --- @return #UNIT self -function UNIT:FindByName( UnitName ) - - local UnitFound = _DATABASE:FindUnit( UnitName ) - return UnitFound -end - ---- Return the name of the UNIT. --- @param #UNIT self --- @return #string The UNIT name. -function UNIT:Name() - - return self.UnitName -end - - ---- @param #UNIT self --- @return Dcs.DCSWrapper.Unit#Unit -function UNIT:GetDCSObject() - - local DCSUnit = Unit.getByName( self.UnitName ) - - if DCSUnit then - return DCSUnit - end - - return nil -end - ---- Respawn the @{Unit} using a (tweaked) template of the parent Group. --- --- This function will: --- --- * Get the current position and heading of the group. --- * When the unit is alive, it will tweak the template x, y and heading coordinates of the group and the embedded units to the current units positions. --- * Then it will respawn the re-modelled group. --- --- @param #UNIT self --- @param Dcs.DCSTypes#Vec3 SpawnVec3 The position where to Spawn the new Unit at. --- @param #number Heading The heading of the unit respawn. -function UNIT:ReSpawn( SpawnVec3, Heading ) - - local SpawnGroupTemplate = UTILS.DeepCopy( _DATABASE:GetGroupTemplateFromUnitName( self:Name() ) ) - self:T( SpawnGroupTemplate ) - - local SpawnGroup = self:GetGroup() - - if SpawnGroup then - - local Vec3 = SpawnGroup:GetVec3() - SpawnGroupTemplate.x = SpawnVec3.x - SpawnGroupTemplate.y = SpawnVec3.z - - self:E( #SpawnGroupTemplate.units ) - for UnitID, UnitData in pairs( SpawnGroup:GetUnits() ) do - local GroupUnit = UnitData -- #UNIT - self:E( GroupUnit:GetName() ) - if GroupUnit:IsAlive() then - local GroupUnitVec3 = GroupUnit:GetVec3() - local GroupUnitHeading = GroupUnit:GetHeading() - SpawnGroupTemplate.units[UnitID].alt = GroupUnitVec3.y - SpawnGroupTemplate.units[UnitID].x = GroupUnitVec3.x - SpawnGroupTemplate.units[UnitID].y = GroupUnitVec3.z - SpawnGroupTemplate.units[UnitID].heading = GroupUnitHeading - self:E( { UnitID, SpawnGroupTemplate.units[UnitID], SpawnGroupTemplate.units[UnitID] } ) - end - end - end - - for UnitTemplateID, UnitTemplateData in pairs( SpawnGroupTemplate.units ) do - self:T( UnitTemplateData.name ) - if UnitTemplateData.name == self:Name() then - self:T("Adjusting") - SpawnGroupTemplate.units[UnitTemplateID].alt = SpawnVec3.y - SpawnGroupTemplate.units[UnitTemplateID].x = SpawnVec3.x - SpawnGroupTemplate.units[UnitTemplateID].y = SpawnVec3.z - SpawnGroupTemplate.units[UnitTemplateID].heading = Heading - self:E( { UnitTemplateID, SpawnGroupTemplate.units[UnitTemplateID], SpawnGroupTemplate.units[UnitTemplateID] } ) - else - self:E( SpawnGroupTemplate.units[UnitTemplateID].name ) - local GroupUnit = UNIT:FindByName( SpawnGroupTemplate.units[UnitTemplateID].name ) -- #UNIT - if GroupUnit and GroupUnit:IsAlive() then - local GroupUnitVec3 = GroupUnit:GetVec3() - local GroupUnitHeading = GroupUnit:GetHeading() - UnitTemplateData.alt = GroupUnitVec3.y - UnitTemplateData.x = GroupUnitVec3.x - UnitTemplateData.y = GroupUnitVec3.z - UnitTemplateData.heading = GroupUnitHeading - else - if SpawnGroupTemplate.units[UnitTemplateID].name ~= self:Name() then - self:T("nilling") - SpawnGroupTemplate.units[UnitTemplateID].delete = true - end - end - end - end - - -- Remove obscolete units from the group structure - local i = 1 - while i <= #SpawnGroupTemplate.units do - - local UnitTemplateData = SpawnGroupTemplate.units[i] - self:T( UnitTemplateData.name ) - - if UnitTemplateData.delete then - table.remove( SpawnGroupTemplate.units, i ) - else - i = i + 1 - end - end - - _DATABASE:Spawn( SpawnGroupTemplate ) -end - - - ---- Returns if the unit is activated. --- @param #UNIT self --- @return #boolean true if Unit is activated. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:IsActive() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - - local UnitIsActive = DCSUnit:isActive() - return UnitIsActive - end - - return nil -end - ---- Returns if the Unit is alive. --- If the Unit is not alive, nil is returned. --- If the Unit is alive and active, true is returned. --- If the Unit is alive but not active, false is returned. --- @param #UNIT self --- @return #boolean true if Unit is alive and active. --- @return #boolean false if Unit is alive but not active. --- @return #nil if the Unit is not existing or is not alive. -function UNIT:IsAlive() - self:F3( self.UnitName ) - - local DCSUnit = self:GetDCSObject() -- Dcs.DCSUnit#Unit - - if DCSUnit then - local UnitIsAlive = DCSUnit:isExist() and DCSUnit:isActive() - return UnitIsAlive - end - - return nil -end - - - ---- Returns the Unit's callsign - the localized string. --- @param #UNIT self --- @return #string The Callsign of the Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetCallsign() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitCallSign = DCSUnit:getCallsign() - return UnitCallSign - end - - self:E( self.ClassName .. " " .. self.UnitName .. " not found!" ) - return nil -end - - ---- Returns name of the player that control the unit or nil if the unit is controlled by A.I. --- @param #UNIT self --- @return #string Player Name --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPlayerName() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - - local PlayerName = DCSUnit:getPlayerName() - if PlayerName == nil then - PlayerName = "" - end - return PlayerName - end - - return nil - -end - ---- Returns the unit's number in the group. --- The number is the same number the unit has in ME. --- It may not be changed during the mission. --- If any unit in the group is destroyed, the numbers of another units will not be changed. --- @param #UNIT self --- @return #number The Unit number. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetNumber() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitNumber = DCSUnit:getNumber() - return UnitNumber - end - - return nil -end - ---- Returns the unit's group if it exist and nil otherwise. --- @param Wrapper.Unit#UNIT self --- @return Wrapper.Group#GROUP The Group of the Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetGroup() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitGroup = GROUP:Find( DCSUnit:getGroup() ) - return UnitGroup - end - - return nil -end - - --- Need to add here functions to check if radar is on and which object etc. - ---- Returns the prefix name of the DCS Unit. A prefix name is a part of the name before a '#'-sign. --- DCS Units spawned with the @{SPAWN} class contain a '#'-sign to indicate the end of the (base) DCS Unit name. --- The spawn sequence number and unit number are contained within the name after the '#' sign. --- @param #UNIT self --- @return #string The name of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPrefix() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPrefix = string.match( self.UnitName, ".*#" ):sub( 1, -2 ) - self:T3( UnitPrefix ) - return UnitPrefix - end - - return nil -end - ---- Returns the Unit's ammunition. --- @param #UNIT self --- @return Dcs.DCSWrapper.Unit#Unit.Ammo --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetAmmo() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitAmmo = DCSUnit:getAmmo() - return UnitAmmo - end - - return nil -end - ---- Returns the unit sensors. --- @param #UNIT self --- @return Dcs.DCSWrapper.Unit#Unit.Sensors --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetSensors() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitSensors = DCSUnit:getSensors() - return UnitSensors - end - - return nil -end - --- Need to add here a function per sensortype --- unit:hasSensors(Unit.SensorType.RADAR, Unit.RadarType.AS) - ---- Returns if the unit has sensors of a certain type. --- @param #UNIT self --- @return #boolean returns true if the unit has specified types of sensors. This function is more preferable than Unit.getSensors() if you don't want to get information about all the unit's sensors, and just want to check if the unit has specified types of sensors. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:HasSensors( ... ) - self:F2( arg ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local HasSensors = DCSUnit:hasSensors( unpack( arg ) ) - return HasSensors - end - - return nil -end - ---- Returns if the unit is SEADable. --- @param #UNIT self --- @return #boolean returns true if the unit is SEADable. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:HasSEAD() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitSEADAttributes = DCSUnit:getDesc().attributes - - local HasSEAD = false - if UnitSEADAttributes["RADAR_BAND1_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND1_FOR_ARM"] == true or - UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] == true then - HasSEAD = true - end - return HasSEAD - end - - return nil -end - ---- Returns two values: --- --- * First value indicates if at least one of the unit's radar(s) is on. --- * Second value is the object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. --- @param #UNIT self --- @return #boolean Indicates if at least one of the unit's radar(s) is on. --- @return Dcs.DCSWrapper.Object#Object The object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetRadar() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitRadarOn, UnitRadarObject = DCSUnit:getRadar() - return UnitRadarOn, UnitRadarObject - end - - return nil, nil -end - ---- Returns relative amount of fuel (from 0.0 to 1.0) the UNIT has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. --- @param #UNIT self --- @return #number The relative amount of fuel (from 0.0 to 1.0). --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetFuel() - self:F( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitFuel = DCSUnit:getFuel() - return UnitFuel - end - - return nil -end - ---- Returns the UNIT in a UNIT list of one element. --- @param #UNIT self --- @return #list The UNITs wrappers. -function UNIT:GetUnits() - self:F2( { self.UnitName } ) - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local DCSUnits = DCSUnit:getUnits() - local Units = {} - Units[1] = UNIT:Find( DCSUnit ) - self:T3( Units ) - return Units - end - - return nil -end - - ---- Returns the unit's health. Dead units has health <= 1.0. --- @param #UNIT self --- @return #number The Unit's health value. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetLife() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitLife = DCSUnit:getLife() - return UnitLife - end - - return -1 -end - ---- Returns the Unit's initial health. --- @param #UNIT self --- @return #number The Unit's initial health value. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetLife0() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitLife0 = DCSUnit:getLife0() - return UnitLife0 - end - - return 0 -end - ---- Returns the category name of the #UNIT. --- @param #UNIT self --- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship -function UNIT:GetCategoryName() - self:F3( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - if DCSUnit then - local CategoryNames = { - [Unit.Category.AIRPLANE] = "Airplane", - [Unit.Category.HELICOPTER] = "Helicopter", - [Unit.Category.GROUND_UNIT] = "Ground Unit", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - } - local UnitCategory = DCSUnit:getDesc().category - self:T3( UnitCategory ) - - return CategoryNames[UnitCategory] - end - - return nil -end - - ---- Returns the Unit's A2G threat level on a scale from 1 to 10 ... --- The following threat levels are foreseen: --- --- * Threat level 0: Unit is unarmed. --- * Threat level 1: Unit is infantry. --- * Threat level 2: Unit is an infantry vehicle. --- * Threat level 3: Unit is ground artillery. --- * Threat level 4: Unit is a tank. --- * Threat level 5: Unit is a modern tank or ifv with ATGM. --- * Threat level 6: Unit is a AAA. --- * Threat level 7: Unit is a SAM or manpad, IR guided. --- * Threat level 8: Unit is a Short Range SAM, radar guided. --- * Threat level 9: Unit is a Medium Range SAM, radar guided. --- * Threat level 10: Unit is a Long Range SAM, radar guided. --- @param #UNIT self -function UNIT:GetThreatLevel() - - - local ThreatLevel = 0 - local ThreatText = "" - - local Descriptor = self:GetDesc() - - if Descriptor then - - local Attributes = Descriptor.attributes - self:T( Attributes ) - - if self:IsGround() then - - self:T( "Ground" ) - - local ThreatLevels = { - "Unarmed", - "Infantry", - "Old Tanks & APCs", - "Tanks & IFVs without ATGM", - "Tanks & IFV with ATGM", - "Modern Tanks", - "AAA", - "IR Guided SAMs", - "SR SAMs", - "MR SAMs", - "LR SAMs" - } - - - if Attributes["LR SAM"] then ThreatLevel = 10 - elseif Attributes["MR SAM"] then ThreatLevel = 9 - elseif Attributes["SR SAM"] and - not Attributes["IR Guided SAM"] then ThreatLevel = 8 - elseif ( Attributes["SR SAM"] or Attributes["MANPADS"] ) and - Attributes["IR Guided SAM"] then ThreatLevel = 7 - elseif Attributes["AAA"] then ThreatLevel = 6 - elseif Attributes["Modern Tanks"] then ThreatLevel = 5 - elseif ( Attributes["Tanks"] or Attributes["IFV"] ) and - Attributes["ATGM"] then ThreatLevel = 4 - elseif ( Attributes["Tanks"] or Attributes["IFV"] ) and - not Attributes["ATGM"] then ThreatLevel = 3 - elseif Attributes["Old Tanks"] or Attributes["APC"] or Attributes["Artillery"] then ThreatLevel = 2 - elseif Attributes["Infantry"] then ThreatLevel = 1 - end - - ThreatText = ThreatLevels[ThreatLevel+1] - end - - if self:IsAir() then - - self:T( "Air" ) - - local ThreatLevels = { - "Unarmed", - "Tanker", - "AWACS", - "Transport Helicopter", - "UAV", - "Bomber", - "Strategic Bomber", - "Attack Helicopter", - "Battleplane", - "Multirole Fighter", - "Fighter" - } - - - if Attributes["Fighters"] then ThreatLevel = 10 - elseif Attributes["Multirole fighters"] then ThreatLevel = 9 - elseif Attributes["Battleplanes"] then ThreatLevel = 8 - elseif Attributes["Attack helicopters"] then ThreatLevel = 7 - elseif Attributes["Strategic bombers"] then ThreatLevel = 6 - elseif Attributes["Bombers"] then ThreatLevel = 5 - elseif Attributes["UAVs"] then ThreatLevel = 4 - elseif Attributes["Transport helicopters"] then ThreatLevel = 3 - elseif Attributes["AWACS"] then ThreatLevel = 2 - elseif Attributes["Tankers"] then ThreatLevel = 1 - end - - ThreatText = ThreatLevels[ThreatLevel+1] - end - - if self:IsShip() then - - self:T( "Ship" ) - - --["Aircraft Carriers"] = {"Heavy armed ships",}, - --["Cruisers"] = {"Heavy armed ships",}, - --["Destroyers"] = {"Heavy armed ships",}, - --["Frigates"] = {"Heavy armed ships",}, - --["Corvettes"] = {"Heavy armed ships",}, - --["Heavy armed ships"] = {"Armed ships", "Armed Air Defence", "HeavyArmoredUnits",}, - --["Light armed ships"] = {"Armed ships","NonArmoredUnits"}, - --["Armed ships"] = {"Ships"}, - --["Unarmed ships"] = {"Ships","HeavyArmoredUnits",}, - - local ThreatLevels = { - "Unarmed ship", - "Light armed ships", - "Corvettes", - "", - "Frigates", - "", - "Cruiser", - "", - "Destroyer", - "", - "Aircraft Carrier" - } - - - if Attributes["Aircraft Carriers"] then ThreatLevel = 10 - elseif Attributes["Destroyers"] then ThreatLevel = 8 - elseif Attributes["Cruisers"] then ThreatLevel = 6 - elseif Attributes["Frigates"] then ThreatLevel = 4 - elseif Attributes["Corvettes"] then ThreatLevel = 2 - elseif Attributes["Light armed ships"] then ThreatLevel = 1 - end - - ThreatText = ThreatLevels[ThreatLevel+1] - end - end - - self:T2( ThreatLevel ) - return ThreatLevel, ThreatText - -end - - --- Is functions - ---- Returns true if the unit is within a @{Zone}. --- @param #UNIT self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is within the @{Zone#ZONE_BASE} -function UNIT:IsInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = Zone:IsVec3InZone( self:GetVec3() ) - - self:T2( { IsInZone } ) - return IsInZone - end - - return false -end - ---- Returns true if the unit is not within a @{Zone}. --- @param #UNIT self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is not within the @{Zone#ZONE_BASE} -function UNIT:IsNotInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = not Zone:IsVec3InZone( self:GetVec3() ) - - self:T( { IsInZone } ) - return IsInZone - else - return false - end -end - - ---- Returns true if there is an **other** DCS Unit within a radius of the current 2D point of the DCS Unit. --- @param #UNIT self --- @param #UNIT AwaitUnit The other UNIT wrapper object. --- @param Radius The radius in meters with the DCS Unit in the centre. --- @return true If the other DCS Unit is within the radius of the 2D point of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:OtherUnitInRadius( AwaitUnit, Radius ) - self:F2( { self.UnitName, AwaitUnit.UnitName, Radius } ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitVec3 = self:GetVec3() - local AwaitUnitVec3 = AwaitUnit:GetVec3() - - if (((UnitVec3.x - AwaitUnitVec3.x)^2 + (UnitVec3.z - AwaitUnitVec3.z)^2)^0.5 <= Radius) then - self:T3( "true" ) - return true - else - self:T3( "false" ) - return false - end - end - - return nil -end - - - - - --- Is methods - ---- Returns if the unit is of an air category. --- If the unit is a helicopter or a plane, then this method will return true, otherwise false. --- @param #UNIT self --- @return #boolean Air category evaluation result. -function UNIT:IsAir() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitDescriptor = DCSUnit:getDesc() - self:T3( { UnitDescriptor.category, Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) - - local IsAirResult = ( UnitDescriptor.category == Unit.Category.AIRPLANE ) or ( UnitDescriptor.category == Unit.Category.HELICOPTER ) - - self:T3( IsAirResult ) - return IsAirResult - end - - return nil -end - ---- Returns if the unit is of an ground category. --- If the unit is a ground vehicle or infantry, this method will return true, otherwise false. --- @param #UNIT self --- @return #boolean Ground category evaluation result. -function UNIT:IsGround() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitDescriptor = DCSUnit:getDesc() - self:T3( { UnitDescriptor.category, Unit.Category.GROUND_UNIT } ) - - local IsGroundResult = ( UnitDescriptor.category == Unit.Category.GROUND_UNIT ) - - self:T3( IsGroundResult ) - return IsGroundResult - end - - return nil -end - ---- Returns if the unit is a friendly unit. --- @param #UNIT self --- @return #boolean IsFriendly evaluation result. -function UNIT:IsFriendly( FriendlyCoalition ) - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitCoalition = DCSUnit:getCoalition() - self:T3( { UnitCoalition, FriendlyCoalition } ) - - local IsFriendlyResult = ( UnitCoalition == FriendlyCoalition ) - - self:E( IsFriendlyResult ) - return IsFriendlyResult - end - - return nil -end - ---- Returns if the unit is of a ship category. --- If the unit is a ship, this method will return true, otherwise false. --- @param #UNIT self --- @return #boolean Ship category evaluation result. -function UNIT:IsShip() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitDescriptor = DCSUnit:getDesc() - self:T3( { UnitDescriptor.category, Unit.Category.SHIP } ) - - local IsShipResult = ( UnitDescriptor.category == Unit.Category.SHIP ) - - self:T3( IsShipResult ) - return IsShipResult - end - - return nil -end - ---- Returns true if the UNIT is in the air. --- @param Wrapper.Positionable#UNIT self --- @return #boolean true if in the air. --- @return #nil The UNIT is not existing or alive. -function UNIT:InAir() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitInAir = DCSUnit:inAir() - self:T3( UnitInAir ) - return UnitInAir - end - - return nil -end - -do -- Event Handling - - --- Subscribe to a DCS Event. - -- @param #UNIT self - -- @param Core.Event#EVENTS Event - -- @param #function EventFunction (optional) The function to be called when the event occurs for the unit. - -- @return #UNIT - function UNIT:HandleEvent( Event, EventFunction ) - - self:EventDispatcher():OnEventForUnit( self:GetName(), EventFunction, self, Event ) - - return self - end - - --- UnSubscribe to a DCS event. - -- @param #UNIT self - -- @param Core.Event#EVENTS Event - -- @return #UNIT - function UNIT:UnHandleEvent( Event ) - - self:EventDispatcher():RemoveForUnit( self:GetName(), self, Event ) - - return self - end - - --- Reset the subscriptions. - -- @param #UNIT self - -- @return #UNIT - function UNIT:ResetEvents() - - self:EventDispatcher():Reset( self ) - - return self - end - -end - -do -- Detection - - --- Returns if a unit is detecting the TargetUnit. - -- @param #UNIT self - -- @param #UNIT TargetUnit - -- @return #boolean true If the TargetUnit is detected by the unit, otherwise false. - function UNIT:IsDetected( TargetUnit ) --R2.1 - - local TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity = self:IsTargetDetected( TargetUnit:GetDCSObject() ) - - return TargetIsDetected - end - - --- Returns if a unit has Line of Sight (LOS) with the TargetUnit. - -- @param #UNIT self - -- @param #UNIT TargetUnit - -- @return #boolean true If the TargetUnit has LOS with the unit, otherwise false. - function UNIT:IsLOS( TargetUnit ) --R2.1 - - local IsLOS = self:GetPointVec3():IsLOS( TargetUnit:GetPointVec3() ) - - return IsLOS - end - - -end--- **Wrapper** -- CLIENT wraps DCS Unit objects acting as a __Client__ or __Player__ within a mission. --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- === --- --- @module Client - - ---- The CLIENT class --- @type CLIENT --- @extends Wrapper.Unit#UNIT - - ---- # CLIENT class, extends @{Unit#UNIT} --- --- Clients are those **Units** defined within the Mission Editor that have the skillset defined as __Client__ or __Player__. --- Note that clients are NOT the same as Units, they are NOT necessarily alive. --- The CLIENT class is a wrapper class to handle the DCS Unit objects that have the skillset defined as __Client__ or __Player__: --- --- * Wraps the DCS Unit objects with skill level set to Player or Client. --- * Support all DCS Unit APIs. --- * Enhance with Unit specific APIs not in the DCS Group API set. --- * When player joins Unit, execute alive init logic. --- * Handles messages to players. --- * Manage the "state" of the DCS Unit. --- --- Clients are being used by the @{MISSION} class to follow players and register their successes. --- --- ## CLIENT reference methods --- --- For each DCS Unit having skill level Player or Client, a CLIENT wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts). --- --- The CLIENT class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference --- using the DCS Unit or the DCS UnitName. --- --- Another thing to know is that CLIENT objects do not "contain" the DCS Unit object. --- The CLIENT methods will reference the DCS Unit object by name when it is needed during API execution. --- If the DCS Unit object does not exist or is nil, the CLIENT methods will return nil and log an exception in the DCS.log file. --- --- The CLIENT class provides the following functions to retrieve quickly the relevant CLIENT instance: --- --- * @{#CLIENT.Find}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit object. --- * @{#CLIENT.FindByName}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit name. --- --- **IMPORTANT: ONE SHOULD NEVER SANATIZE these CLIENT OBJECT REFERENCES! (make the CLIENT object references nil).** --- --- @field #CLIENT -CLIENT = { - ONBOARDSIDE = { - NONE = 0, - LEFT = 1, - RIGHT = 2, - BACK = 3, - FRONT = 4 - }, - ClassName = "CLIENT", - ClientName = nil, - ClientAlive = false, - ClientTransport = false, - ClientBriefingShown = false, - _Menus = {}, - _Tasks = {}, - Messages = { - } -} - - ---- Finds a CLIENT from the _DATABASE using the relevant DCS Unit. --- @param #CLIENT self --- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor. --- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client. --- @return #CLIENT --- @usage --- -- Create new Clients. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- Mission:AddGoal( DeploySA6TroopsGoal ) --- --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) -function CLIENT:Find( DCSUnit, Error ) - local ClientName = DCSUnit:getName() - local ClientFound = _DATABASE:FindClient( ClientName ) - - if ClientFound then - ClientFound:F( ClientName ) - return ClientFound - end - - if not Error then - error( "CLIENT not found for: " .. ClientName ) - end -end - - ---- Finds a CLIENT from the _DATABASE using the relevant Client Unit Name. --- As an optional parameter, a briefing text can be given also. --- @param #CLIENT self --- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor. --- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client. --- @param #boolean Error A flag that indicates whether an error should be raised if the CLIENT cannot be found. By default an error will be raised. --- @return #CLIENT --- @usage --- -- Create new Clients. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- Mission:AddGoal( DeploySA6TroopsGoal ) --- --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) -function CLIENT:FindByName( ClientName, ClientBriefing, Error ) - local ClientFound = _DATABASE:FindClient( ClientName ) - - if ClientFound then - ClientFound:F( { ClientName, ClientBriefing } ) - ClientFound:AddBriefing( ClientBriefing ) - ClientFound.MessageSwitch = true - - return ClientFound - end - - if not Error then - error( "CLIENT not found for: " .. ClientName ) - end -end - -function CLIENT:Register( ClientName ) - local self = BASE:Inherit( self, UNIT:Register( ClientName ) ) - - self:F( ClientName ) - self.ClientName = ClientName - self.MessageSwitch = true - self.ClientAlive2 = false - - --self.AliveCheckScheduler = routines.scheduleFunction( self._AliveCheckScheduler, { self }, timer.getTime() + 1, 5 ) - self.AliveCheckScheduler = SCHEDULER:New( self, self._AliveCheckScheduler, { "Client Alive " .. ClientName }, 1, 5 ) - - self:E( self ) - return self -end - - ---- Transport defines that the Client is a Transport. Transports show cargo. --- @param #CLIENT self --- @return #CLIENT -function CLIENT:Transport() - self:F() - - self.ClientTransport = true - return self -end - ---- AddBriefing adds a briefing to a CLIENT when a player joins a mission. --- @param #CLIENT self --- @param #string ClientBriefing is the text defining the Mission briefing. --- @return #CLIENT self -function CLIENT:AddBriefing( ClientBriefing ) - self:F( ClientBriefing ) - self.ClientBriefing = ClientBriefing - self.ClientBriefingShown = false - - return self -end - ---- Show the briefing of a CLIENT. --- @param #CLIENT self --- @return #CLIENT self -function CLIENT:ShowBriefing() - self:F( { self.ClientName, self.ClientBriefingShown } ) - - if not self.ClientBriefingShown then - self.ClientBriefingShown = true - local Briefing = "" - if self.ClientBriefing then - Briefing = Briefing .. self.ClientBriefing - end - Briefing = Briefing .. " Press [LEFT ALT]+[B] to view the complete mission briefing." - self:Message( Briefing, 60, "Briefing" ) - end - - return self -end - ---- Show the mission briefing of a MISSION to the CLIENT. --- @param #CLIENT self --- @param #string MissionBriefing --- @return #CLIENT self -function CLIENT:ShowMissionBriefing( MissionBriefing ) - self:F( { self.ClientName } ) - - if MissionBriefing then - self:Message( MissionBriefing, 60, "Mission Briefing" ) - end - - return self -end - - - ---- Resets a CLIENT. --- @param #CLIENT self --- @param #string ClientName Name of the Group as defined within the Mission Editor. The Group must have a Unit with the type Client. -function CLIENT:Reset( ClientName ) - self:F() - self._Menus = {} -end - --- Is Functions - ---- Checks if the CLIENT is a multi-seated UNIT. --- @param #CLIENT self --- @return #boolean true if multi-seated. -function CLIENT:IsMultiSeated() - self:F( self.ClientName ) - - local ClientMultiSeatedTypes = { - ["Mi-8MT"] = "Mi-8MT", - ["UH-1H"] = "UH-1H", - ["P-51B"] = "P-51B" - } - - if self:IsAlive() then - local ClientTypeName = self:GetClientGroupUnit():GetTypeName() - if ClientMultiSeatedTypes[ClientTypeName] then - return true - end - end - - return false -end - ---- Checks for a client alive event and calls a function on a continuous basis. --- @param #CLIENT self --- @param #function CallBackFunction Create a function that will be called when a player joins the slot. --- @return #CLIENT -function CLIENT:Alive( CallBackFunction, ... ) - self:F() - - self.ClientCallBack = CallBackFunction - self.ClientParameters = arg - - return self -end - ---- @param #CLIENT self -function CLIENT:_AliveCheckScheduler( SchedulerName ) - self:F3( { SchedulerName, self.ClientName, self.ClientAlive2, self.ClientBriefingShown, self.ClientCallBack } ) - - if self:IsAlive() then - if self.ClientAlive2 == false then - self:ShowBriefing() - if self.ClientCallBack then - self:T("Calling Callback function") - self.ClientCallBack( self, unpack( self.ClientParameters ) ) - end - self.ClientAlive2 = true - end - else - if self.ClientAlive2 == true then - self.ClientAlive2 = false - end - end - - return true -end - ---- Return the DCSGroup of a Client. --- This function is modified to deal with a couple of bugs in DCS 1.5.3 --- @param #CLIENT self --- @return Dcs.DCSWrapper.Group#Group -function CLIENT:GetDCSGroup() - self:F3() - --- local ClientData = Group.getByName( self.ClientName ) --- if ClientData and ClientData:isExist() then --- self:T( self.ClientName .. " : group found!" ) --- return ClientData --- else --- return nil --- end - - local ClientUnit = Unit.getByName( self.ClientName ) - - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - self:T3( { "CoalitionData:", CoalitionData } ) - for UnitId, UnitData in pairs( CoalitionData ) do - self:T3( { "UnitData:", UnitData } ) - if UnitData and UnitData:isExist() then - - --self:E(self.ClientName) - if ClientUnit then - local ClientGroup = ClientUnit:getGroup() - if ClientGroup then - self:T3( "ClientGroup = " .. self.ClientName ) - if ClientGroup:isExist() and UnitData:getGroup():isExist() then - if ClientGroup:getID() == UnitData:getGroup():getID() then - self:T3( "Normal logic" ) - self:T3( self.ClientName .. " : group found!" ) - self.ClientGroupID = ClientGroup:getID() - self.ClientGroupName = ClientGroup:getName() - return ClientGroup - end - else - -- Now we need to resolve the bugs in DCS 1.5 ... - -- Consult the database for the units of the Client Group. (ClientGroup:getUnits() returns nil) - self:T3( "Bug 1.5 logic" ) - local ClientGroupTemplate = _DATABASE.Templates.Units[self.ClientName].GroupTemplate - self.ClientGroupID = ClientGroupTemplate.groupId - self.ClientGroupName = _DATABASE.Templates.Units[self.ClientName].GroupName - self:T3( self.ClientName .. " : group found in bug 1.5 resolvement logic!" ) - return ClientGroup - end - -- else - -- error( "Client " .. self.ClientName .. " not found!" ) - end - else - --self:E( { "Client not found!", self.ClientName } ) - end - end - end - end - - -- For non player clients - if ClientUnit then - local ClientGroup = ClientUnit:getGroup() - if ClientGroup then - self:T3( "ClientGroup = " .. self.ClientName ) - if ClientGroup:isExist() then - self:T3( "Normal logic" ) - self:T3( self.ClientName .. " : group found!" ) - return ClientGroup - end - end - end - - self.ClientGroupID = nil - self.ClientGroupUnit = nil - - return nil -end - - --- TODO: Check Dcs.DCSTypes#Group.ID ---- Get the group ID of the client. --- @param #CLIENT self --- @return Dcs.DCSTypes#Group.ID -function CLIENT:GetClientGroupID() - - local ClientGroup = self:GetDCSGroup() - - --self:E( self.ClientGroupID ) -- Determined in GetDCSGroup() - return self.ClientGroupID -end - - ---- Get the name of the group of the client. --- @param #CLIENT self --- @return #string -function CLIENT:GetClientGroupName() - - local ClientGroup = self:GetDCSGroup() - - self:T( self.ClientGroupName ) -- Determined in GetDCSGroup() - return self.ClientGroupName -end - ---- Returns the UNIT of the CLIENT. --- @param #CLIENT self --- @return Wrapper.Unit#UNIT -function CLIENT:GetClientGroupUnit() - self:F2() - - local ClientDCSUnit = Unit.getByName( self.ClientName ) - - self:T( self.ClientDCSUnit ) - if ClientDCSUnit and ClientDCSUnit:isExist() then - local ClientUnit = _DATABASE:FindUnit( self.ClientName ) - self:T2( ClientUnit ) - return ClientUnit - end -end - ---- Returns the DCSUnit of the CLIENT. --- @param #CLIENT self --- @return Dcs.DCSTypes#Unit -function CLIENT:GetClientGroupDCSUnit() - self:F2() - - local ClientDCSUnit = Unit.getByName( self.ClientName ) - - if ClientDCSUnit and ClientDCSUnit:isExist() then - self:T2( ClientDCSUnit ) - return ClientDCSUnit - end -end - - ---- Evaluates if the CLIENT is a transport. --- @param #CLIENT self --- @return #boolean true is a transport. -function CLIENT:IsTransport() - self:F() - return self.ClientTransport -end - ---- Shows the @{AI_Cargo#CARGO} contained within the CLIENT to the player as a message. --- The @{AI_Cargo#CARGO} is shown using the @{Message#MESSAGE} distribution system. --- @param #CLIENT self -function CLIENT:ShowCargo() - self:F() - - local CargoMsg = "" - - for CargoName, Cargo in pairs( CARGOS ) do - if self == Cargo:IsLoadedInClient() then - CargoMsg = CargoMsg .. Cargo.CargoName .. " Type:" .. Cargo.CargoType .. " Weight: " .. Cargo.CargoWeight .. "\n" - end - end - - if CargoMsg == "" then - CargoMsg = "empty" - end - - self:Message( CargoMsg, 15, "Co-Pilot: Cargo Status", 30 ) - -end - --- TODO (1) I urgently need to revise this. ---- A local function called by the DCS World Menu system to switch off messages. -function CLIENT.SwitchMessages( PrmTable ) - PrmTable[1].MessageSwitch = PrmTable[2] -end - ---- The main message driver for the CLIENT. --- This function displays various messages to the Player logged into the CLIENT through the DCS World Messaging system. --- @param #CLIENT self --- @param #string Message is the text describing the message. --- @param #number MessageDuration is the duration in seconds that the Message should be displayed. --- @param #string MessageCategory is the category of the message (the title). --- @param #number MessageInterval is the interval in seconds between the display of the @{Message#MESSAGE} when the CLIENT is in the air. --- @param #string MessageID is the identifier of the message when displayed with intervals. -function CLIENT:Message( Message, MessageDuration, MessageCategory, MessageInterval, MessageID ) - self:F( { Message, MessageDuration, MessageCategory, MessageInterval } ) - - if self.MessageSwitch == true then - if MessageCategory == nil then - MessageCategory = "Messages" - end - if MessageID ~= nil then - if self.Messages[MessageID] == nil then - self.Messages[MessageID] = {} - self.Messages[MessageID].MessageId = MessageID - self.Messages[MessageID].MessageTime = timer.getTime() - self.Messages[MessageID].MessageDuration = MessageDuration - if MessageInterval == nil then - self.Messages[MessageID].MessageInterval = 600 - else - self.Messages[MessageID].MessageInterval = MessageInterval - end - MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) - else - if self:GetClientGroupDCSUnit() and not self:GetClientGroupDCSUnit():inAir() then - if timer.getTime() - self.Messages[MessageID].MessageTime >= self.Messages[MessageID].MessageDuration + 10 then - MESSAGE:New( Message, MessageDuration , MessageCategory):ToClient( self ) - self.Messages[MessageID].MessageTime = timer.getTime() - end - else - if timer.getTime() - self.Messages[MessageID].MessageTime >= self.Messages[MessageID].MessageDuration + self.Messages[MessageID].MessageInterval then - MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) - self.Messages[MessageID].MessageTime = timer.getTime() - end - end - end - else - MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) - end - end -end ---- **Wrapper** -- STATIC wraps the DCS StaticObject class. --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- ==== --- --- @module Static - - ---- @type STATIC --- @extends Wrapper.Positionable#POSITIONABLE - ---- # STATIC class, extends @{Positionable#POSITIONABLE} --- --- Statics are **Static Units** defined within the Mission Editor. --- Note that Statics are almost the same as Units, but they don't have a controller. --- The @{Static#STATIC} class is a wrapper class to handle the DCS Static objects: --- --- * Wraps the DCS Static objects. --- * Support all DCS Static APIs. --- * Enhance with Static specific APIs not in the DCS API set. --- --- ## STATIC reference methods --- --- For each DCS Static will have a STATIC wrapper object (instance) within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts). --- --- The STATIC class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference --- using the Static Name. --- --- Another thing to know is that STATIC objects do not "contain" the DCS Static object. --- The STATIc methods will reference the DCS Static object by name when it is needed during API execution. --- If the DCS Static object does not exist or is nil, the STATIC methods will return nil and log an exception in the DCS.log file. --- --- The STATIc class provides the following functions to retrieve quickly the relevant STATIC instance: --- --- * @{#STATIC.FindByName}(): Find a STATIC instance from the _DATABASE object using a DCS Static name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these STATIC OBJECT REFERENCES! (make the STATIC object references nil). --- --- @field #STATIC -STATIC = { - ClassName = "STATIC", -} - - ---- Finds a STATIC from the _DATABASE using the relevant Static Name. --- As an optional parameter, a briefing text can be given also. --- @param #STATIC self --- @param #string StaticName Name of the DCS **Static** as defined within the Mission Editor. --- @param #boolean RaiseError Raise an error if not found. --- @return #STATIC -function STATIC:FindByName( StaticName, RaiseError ) - local StaticFound = _DATABASE:FindStatic( StaticName ) - - self.StaticName = StaticName - - if StaticFound then - StaticFound:F3( { StaticName } ) - - return StaticFound - end - - if RaiseError == nil or RaiseError == true then - error( "STATIC not found for: " .. StaticName ) - end - - return nil -end - -function STATIC:Register( StaticName ) - local self = BASE:Inherit( self, POSITIONABLE:New( StaticName ) ) - self.StaticName = StaticName - return self -end - - -function STATIC:GetDCSObject() - local DCSStatic = StaticObject.getByName( self.StaticName ) - - if DCSStatic then - return DCSStatic - end - - return nil -end - -function STATIC:GetThreatLevel() - - return 1, "Static" -end--- **Wrapper** -- AIRBASE is a wrapper class to handle the DCS Airbase objects. --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- === --- --- @module Airbase - - ---- @type AIRBASE --- @extends Wrapper.Positionable#POSITIONABLE - ---- # AIRBASE class, extends @{Positionable#POSITIONABLE} --- --- AIRBASE is a wrapper class to handle the DCS Airbase objects: --- --- * Support all DCS Airbase APIs. --- * Enhance with Airbase specific APIs not in the DCS Airbase API set. --- --- ## AIRBASE reference methods --- --- For each DCS Airbase object alive within a running mission, a AIRBASE wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts). --- --- The AIRBASE class **does not contain a :New()** method, rather it provides **:Find()** methods to retrieve the object reference --- using the DCS Airbase or the DCS AirbaseName. --- --- Another thing to know is that AIRBASE objects do not "contain" the DCS Airbase object. --- The AIRBASE methods will reference the DCS Airbase object by name when it is needed during API execution. --- If the DCS Airbase object does not exist or is nil, the AIRBASE methods will return nil and log an exception in the DCS.log file. --- --- The AIRBASE class provides the following functions to retrieve quickly the relevant AIRBASE instance: --- --- * @{#AIRBASE.Find}(): Find a AIRBASE instance from the _DATABASE object using a DCS Airbase object. --- * @{#AIRBASE.FindByName}(): Find a AIRBASE instance from the _DATABASE object using a DCS Airbase name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these AIRBASE OBJECT REFERENCES! (make the AIRBASE object references nil). --- --- ## DCS Airbase APIs --- --- The DCS Airbase APIs are used extensively within MOOSE. The AIRBASE class has for each DCS Airbase API a corresponding method. --- To be able to distinguish easily in your code the difference between a AIRBASE API call and a DCS Airbase API call, --- the first letter of the method is also capitalized. So, by example, the DCS Airbase method @{DCSWrapper.Airbase#Airbase.getName}() --- is implemented in the AIRBASE class as @{#AIRBASE.GetName}(). --- --- @field #AIRBASE AIRBASE -AIRBASE = { - ClassName="AIRBASE", - CategoryName = { - [Airbase.Category.AIRDROME] = "Airdrome", - [Airbase.Category.HELIPAD] = "Helipad", - [Airbase.Category.SHIP] = "Ship", - }, - } - ---- @field Caucasus -AIRBASE.Caucasus = { - ["Gelendzhik"] = "Gelendzhik", - ["Krasnodar_Pashkovsky"] = "Krasnodar-Pashkovsky", - ["Sukhumi_Babushara"] = "Sukhumi-Babushara", - ["Gudauta"] = "Gudauta", - ["Batumi"] = "Batumi", - ["Senaki_Kolkhi"] = "Senaki-Kolkhi", - ["Kobuleti"] = "Kobuleti", - ["Kutaisi"] = "Kutaisi", - ["Tbilisi_Lochini"] = "Tbilisi-Lochini", - ["Soganlug"] = "Soganlug", - ["Vaziani"] = "Vaziani", - ["Anapa_Vityazevo"] = "Anapa-Vityazevo", - ["Krasnodar_Center"] = "Krasnodar-Center", - ["Novorossiysk"] = "Novorossiysk", - ["Krymsk"] = "Krymsk", - ["Maykop_Khanskaya"] = "Maykop-Khanskaya", - ["Sochi_Adler"] = "Sochi-Adler", - ["Mineralnye_Vody"] = "Mineralnye Vody", - ["Nalchik"] = "Nalchik", - ["Mozdok"] = "Mozdok", - ["Beslan"] = "Beslan", - } - ---- @field Nevada -AIRBASE.Nevada = { - ["Creech_AFB"] = "Creech AFB", - ["Groom_Lake_AFB"] = "Groom Lake AFB", - ["McCarran_International_Airport"] = "McCarran International Airport", - ["Nellis_AFB"] = "Nellis AFB", - ["Beatty_Airport"] = "Beatty Airport", - ["Boulder_City_Airport"] = "Boulder City Airport", - ["Echo_Bay"] = "Echo Bay", - ["Henderson_Executive_Airport"] = "Henderson Executive Airport", - ["Jean_Airport"] = "Jean Airport", - ["Laughlin_Airport"] = "Laughlin Airport", - ["Lincoln_County"] = "Lincoln County", - ["Mellan_Airstrip"] = "Mellan Airstrip", - ["Mesquite"] = "Mesquite", - ["Mina_Airport_3Q0"] = "Mina Airport 3Q0", - ["North_Las_Vegas"] = "North Las Vegas", - ["Pahute_Mesa_Airstrip"] = "Pahute Mesa Airstrip", - ["Tonopah_Airport"] = "Tonopah Airport", - ["Tonopah_Test_Range_Airfield"] = "Tonopah Test Range Airfield", - } - ---- @field Normandy -AIRBASE.Normandy = { - ["Saint_Pierre_du_Mont"] = "Saint Pierre du Mont", - ["Lignerolles"] = "Lignerolles", - ["Cretteville"] = "Cretteville", - ["Maupertus"] = "Maupertus", - ["Brucheville"] = "Brucheville", - ["Meautis"] = "Meautis", - ["Cricqueville_en_Bessin"] = "Cricqueville-en-Bessin", - ["Lessay"] = "Lessay", - ["Sainte_Laurent_sur_Mer"] = "Sainte-Laurent-sur-Mer", - ["Biniville"] = "Biniville", - ["Cardonville"] = "Cardonville", - ["Deux_Jumeaux"] = "Deux Jumeaux", - ["Chippelle"] = "Chippelle", - ["Beuzeville"] = "Beuzeville", - ["Azeville"] = "Azeville", - ["Picauville"] = "Picauville", - ["Le_Molay"] = "Le Molay", - ["Longues_sur_Mer"] = "Longues-sur-Mer", - ["Carpiquet"] = "Carpiquet", - ["Bazenville"] = "Bazenville", - ["Sainte_Croix_sur_Mer"] = "Sainte-Croix-sur-Mer", - ["Beny_sur_Mer"] = "Beny-sur-Mer", - ["Rucqueville"] = "Rucqueville", - ["Sommervieu"] = "Sommervieu", - ["Lantheuil"] = "Lantheuil", - ["Evreux"] = "Evreux", - ["Chailey"] = "Chailey", - ["Needs_Oar_Point"] = "Needs Oar Point", - ["Funtington"] = "Funtington", - ["Tangmere"] = "Tangmere", - ["Ford"] = "Ford", - } - --- Registration. - ---- Create a new AIRBASE from DCSAirbase. --- @param #AIRBASE self --- @param #string AirbaseName The name of the airbase. --- @return Wrapper.Airbase#AIRBASE -function AIRBASE:Register( AirbaseName ) - - local self = BASE:Inherit( self, POSITIONABLE:New( AirbaseName ) ) - self.AirbaseName = AirbaseName - self.AirbaseZone = ZONE_RADIUS:New( AirbaseName, self:GetVec2(), 8000 ) - return self -end - --- Reference methods. - ---- Finds a AIRBASE from the _DATABASE using a DCSAirbase object. --- @param #AIRBASE self --- @param Dcs.DCSWrapper.Airbase#Airbase DCSAirbase An existing DCS Airbase object reference. --- @return Wrapper.Airbase#AIRBASE self -function AIRBASE:Find( DCSAirbase ) - - local AirbaseName = DCSAirbase:getName() - local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) - return AirbaseFound -end - ---- Find a AIRBASE in the _DATABASE using the name of an existing DCS Airbase. --- @param #AIRBASE self --- @param #string AirbaseName The Airbase Name. --- @return Wrapper.Airbase#AIRBASE self -function AIRBASE:FindByName( AirbaseName ) - - local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) - return AirbaseFound -end - -function AIRBASE:GetDCSObject() - local DCSAirbase = Airbase.getByName( self.AirbaseName ) - - if DCSAirbase then - return DCSAirbase - end - - return nil -end - ---- Get the airbase zone. --- @param #AIRBASE self --- @return Core.Zone#ZONE_RADIUS The zone radius of the airbase. -function AIRBASE:GetZone() - return self.AirbaseZone -end - - - ---- **Wrapper** -- SCENERY models scenery within the DCS simulator. --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- ==== --- --- @module Scenery - - - ---- @type SCENERY --- @extends Wrapper.Positionable#POSITIONABLE - - ---- # SCENERY class, extends @{Positionable#POSITIONABLE} --- --- Scenery objects are defined on the map. --- The @{Scenery#SCENERY} class is a wrapper class to handle the DCS Scenery objects: --- --- * Wraps the DCS Scenery objects. --- * Support all DCS Scenery APIs. --- * Enhance with Scenery specific APIs not in the DCS API set. --- --- @field #SCENERY -SCENERY = { - ClassName = "SCENERY", -} - - -function SCENERY:Register( SceneryName, SceneryObject ) - local self = BASE:Inherit( self, POSITIONABLE:New( SceneryName ) ) - self.SceneryName = SceneryName - self.SceneryObject = SceneryObject - return self -end - -function SCENERY:GetDCSObject() - return self.SceneryObject -end - -function SCENERY:GetThreatLevel() - - return 0, "Scenery" -end ---- **Functional** -- **Administer the SCORING of player achievements, --- and create a CSV file logging the scoring events for use at team or squadron websites.** --- --- ![Banner Image](..\Presentations\SCORING\Dia1.JPG) --- --- === --- --- The @{#SCORING} class administers the scoring of player achievements, --- and creates a CSV file logging the scoring events and results for use at team or squadron websites. --- --- SCORING automatically calculates the threat level of the objects hit and destroyed by players, --- which can be @{Unit}, @{Static) and @{Scenery} objects. --- --- Positive score points are granted when enemy or neutral targets are destroyed. --- Negative score points or penalties are given when a friendly target is hit or destroyed. --- This brings a lot of dynamism in the scoring, where players need to take care to inflict damage on the right target. --- By default, penalties weight heavier in the scoring, to ensure that players don't commit fratricide. --- The total score of the player is calculated by **adding the scores minus the penalties**. --- --- ![Banner Image](..\Presentations\SCORING\Dia4.JPG) --- --- The score value is calculated based on the **threat level of the player** and the **threat level of the target**. --- A calculated score takes the threat level of the target divided by a balanced threat level of the player unit. --- As such, if the threat level of the target is high, and the player threat level is low, a higher score will be given than --- if the threat level of the player would be high too. --- --- ![Banner Image](..\Presentations\SCORING\Dia5.JPG) --- --- When multiple players hit the same target, and finally succeed in destroying the target, then each player who contributed to the target --- destruction, will receive a score. This is important for targets that require significant damage before it can be destroyed, like --- ships or heavy planes. --- --- ![Banner Image](..\Presentations\SCORING\Dia13.JPG) --- --- Optionally, the score values can be **scaled** by a **scale**. Specific scales can be set for positive cores or negative penalties. --- The default range of the scores granted is a value between 0 and 10. The default range of penalties given is a value between 0 and 30. --- --- ![Banner Image](..\Presentations\SCORING\Dia7.JPG) --- --- **Additional scores** can be granted to **specific objects**, when the player(s) destroy these objects. --- --- ![Banner Image](..\Presentations\SCORING\Dia9.JPG) --- --- Various @{Zone}s can be defined for which scores are also granted when objects in that @{Zone} are destroyed. --- This is **specifically useful** to designate **scenery targets on the map** that will generate points when destroyed. --- --- With a small change in MissionScripting.lua, the scoring results can also be logged in a **CSV file**. --- These CSV files can be used to: --- --- * Upload scoring to a database or a BI tool to publish the scoring results to the player community. --- * Upload scoring in an (online) Excel like tool, using pivot tables and pivot charts to show mission results. --- * Share scoring amoung players after the mission to discuss mission results. --- --- Scores can be **reported**. **Menu options** are automatically added to **each player group** when a player joins a client slot or a CA unit. --- Use the radio menu F10 to consult the scores while running the mission. --- Scores can be reported for your user, or an overall score can be reported of all players currently active in the mission. --- --- # 1) @{Scoring#SCORING} class, extends @{Base#BASE} --- --- ## 1.1) Set the destroy score or penalty scale --- --- Score scales can be set for scores granted when enemies or friendlies are destroyed. --- Use the method @{#SCORING.SetScaleDestroyScore}() to set the scale of enemy destroys (positive destroys). --- Use the method @{#SCORING.SetScaleDestroyPenalty}() to set the scale of friendly destroys (negative destroys). --- --- local Scoring = SCORING:New( "Scoring File" ) --- Scoring:SetScaleDestroyScore( 10 ) --- Scoring:SetScaleDestroyPenalty( 40 ) --- --- The above sets the scale for valid scores to 10. So scores will be given in a scale from 0 to 10. --- The penalties will be given in a scale from 0 to 40. --- --- ## 1.2) Define special targets that will give extra scores. --- --- Special targets can be set that will give extra scores to the players when these are destroyed. --- Use the methods @{#SCORING.AddUnitScore}() and @{#SCORING.RemoveUnitScore}() to specify a special additional score for a specific @{Unit}s. --- Use the methods @{#SCORING.AddStaticScore}() and @{#SCORING.RemoveStaticScore}() to specify a special additional score for a specific @{Static}s. --- Use the method @{#SCORING.SetGroupGroup}() to specify a special additional score for a specific @{Group}s. --- --- local Scoring = SCORING:New( "Scoring File" ) --- Scoring:AddUnitScore( UNIT:FindByName( "Unit #001" ), 200 ) --- Scoring:AddStaticScore( STATIC:FindByName( "Static #1" ), 100 ) --- --- The above grants an additional score of 200 points for Unit #001 and an additional 100 points of Static #1 if these are destroyed. --- Note that later in the mission, one can remove these scores set, for example, when the a goal achievement time limit is over. --- For example, this can be done as follows: --- --- Scoring:RemoveUnitScore( UNIT:FindByName( "Unit #001" ) ) --- --- ## 1.3) Define destruction zones that will give extra scores. --- --- Define zones of destruction. Any object destroyed within the zone of the given category will give extra points. --- Use the method @{#SCORING.AddZoneScore}() to add a @{Zone} for additional scoring. --- Use the method @{#SCORING.RemoveZoneScore}() to remove a @{Zone} for additional scoring. --- There are interesting variations that can be achieved with this functionality. For example, if the @{Zone} is a @{Zone#ZONE_UNIT}, --- then the zone is a moving zone, and anything destroyed within that @{Zone} will generate points. --- The other implementation could be to designate a scenery target (a building) in the mission editor surrounded by a @{Zone}, --- just large enough around that building. --- --- ## 1.4) Add extra Goal scores upon an event or a condition. --- --- A mission has goals and achievements. The scoring system provides an API to set additional scores when a goal or achievement event happens. --- Use the method @{#SCORING.AddGoalScore}() to add a score for a Player at any time in your mission. --- --- ## 1.5) Configure fratricide level. --- --- When a player commits too much damage to friendlies, his penalty score will reach a certain level. --- Use the method @{#SCORING.SetFratricide}() to define the level when a player gets kicked. --- By default, the fratricide level is the default penalty mutiplier * 2 for the penalty score. --- --- ## 1.6) Penalty score when a player changes the coalition. --- --- When a player changes the coalition, he can receive a penalty score. --- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition. --- By default, the penalty for changing coalition is the default penalty scale. --- --- ## 1.8) Define output CSV files. --- --- The CSV file is given the name of the string given in the @{#SCORING.New}{} constructor, followed by the .csv extension. --- The file is incrementally saved in the **\\Saved Games\\DCS\\Logs** folder, and has a time stamp indicating each mission run. --- See the following example: --- --- local ScoringFirstMission = SCORING:New( "FirstMission" ) --- local ScoringSecondMission = SCORING:New( "SecondMission" ) --- --- The above documents that 2 Scoring objects are created, ScoringFirstMission and ScoringSecondMission. --- --- ### **IMPORTANT!!!* --- In order to allow DCS world to write CSV files, you need to adapt a configuration file in your DCS world installation **on the server**. --- For this, browse to the **missionscripting.lua** file in your DCS world installation folder. --- For me, this installation folder is in _D:\\Program Files\\Eagle Dynamics\\DCS World\Scripts_. --- --- Edit a few code lines in the MissionScripting.lua file. Comment out the lines **os**, **io** and **lfs**: --- --- do --- --sanitizeModule('os') --- --sanitizeModule('io') --- --sanitizeModule('lfs') --- require = nil --- loadlib = nil --- end --- --- When these lines are not sanitized, functions become available to check the time, and to write files to your system at the above specified location. --- Note that the MissionScripting.lua file provides a warning. So please beware of this warning as outlined by Eagle Dynamics! --- --- --Sanitize Mission Scripting environment --- --This makes unavailable some unsecure functions. --- --Mission downloaded from server to client may contain potentialy harmful lua code that may use these functions. --- --You can remove the code below and make availble these functions at your own risk. --- --- The MOOSE designer cannot take any responsibility of any damage inflicted as a result of the de-sanitization. --- That being said, I hope that the SCORING class provides you with a great add-on to score your squad mates achievements. --- --- ## 1.9) Configure messages. --- --- When players hit or destroy targets, messages are sent. --- Various methods exist to configure: --- --- * Which messages are sent upon the event. --- * Which audience receives the message. --- --- ### 1.9.1) Configure the messages sent upon the event. --- --- Use the following methods to configure when to send messages. By default, all messages are sent. --- --- * @{#SCORING.SetMessagesHit}(): Configure to send messages after a target has been hit. --- * @{#SCORING.SetMessagesDestroy}(): Configure to send messages after a target has been destroyed. --- * @{#SCORING.SetMessagesAddon}(): Configure to send messages for additional score, after a target has been destroyed. --- * @{#SCORING.SetMessagesZone}(): Configure to send messages for additional score, after a target has been destroyed within a given zone. --- --- ### 1.9.2) Configure the audience of the messages. --- --- Use the following methods to configure the audience of the messages. By default, the messages are sent to all players in the mission. --- --- * @{#SCORING.SetMessagesToAll}(): Configure to send messages to all players. --- * @{#SCORING.SetMessagesToCoalition}(): Configure to send messages to only those players within the same coalition as the player. --- --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-02-26: Initial class and API. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **Wingthor (TAW)**: Testing & Advice. --- * **Dutch-Baron (TAW)**: Testing & Advice. --- * **[Whisper](http://forums.eagle.ru/member.php?u=3829): Testing and Advice. --- --- ### Authors: --- --- * **FlightControl**: Concept, Design & Programming. --- --- @module Scoring - - ---- The Scoring class --- @type SCORING --- @field Players A collection of the current players that have joined the game. --- @extends Core.Base#BASE -SCORING = { - ClassName = "SCORING", - ClassID = 0, - Players = {}, -} - -local _SCORINGCoalition = - { - [1] = "Red", - [2] = "Blue", - } - -local _SCORINGCategory = - { - [Unit.Category.AIRPLANE] = "Plane", - [Unit.Category.HELICOPTER] = "Helicopter", - [Unit.Category.GROUND_UNIT] = "Vehicle", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - } - ---- Creates a new SCORING object to administer the scoring achieved by players. --- @param #SCORING self --- @param #string GameName The name of the game. This name is also logged in the CSV score file. --- @return #SCORING self --- @usage --- -- Define a new scoring object for the mission Gori Valley. --- ScoringObject = SCORING:New( "Gori Valley" ) -function SCORING:New( GameName ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) -- #SCORING - - if GameName then - self.GameName = GameName - else - error( "A game name must be given to register the scoring results" ) - end - - - -- Additional Object scores - self.ScoringObjects = {} - - -- Additional Zone scores. - self.ScoringZones = {} - - -- Configure Messages - self:SetMessagesToAll() - self:SetMessagesHit( true ) - self:SetMessagesDestroy( true ) - self:SetMessagesScore( true ) - self:SetMessagesZone( true ) - - -- Scales - self:SetScaleDestroyScore( 10 ) - self:SetScaleDestroyPenalty( 30 ) - - -- Default fratricide penalty level (maximum penalty that can be assigned to a player before he gets kicked). - self:SetFratricide( self.ScaleDestroyPenalty * 3 ) - - -- Default penalty when a player changes coalition. - self:SetCoalitionChangePenalty( self.ScaleDestroyPenalty ) - - self:SetDisplayMessagePrefix() - - -- Event handlers - self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Hit, self._EventOnHit ) - self:HandleEvent( EVENTS.PlayerEnterUnit ) - self:HandleEvent( EVENTS.PlayerLeaveUnit ) - - -- Create the CSV file. - self:OpenCSV( GameName ) - - return self - -end - ---- Set a prefix string that will be displayed at each scoring message sent. --- @param #SCORING self --- @param #string DisplayMessagePrefix (Default="Scoring: ") The scoring prefix string. --- @return #SCORING -function SCORING:SetDisplayMessagePrefix( DisplayMessagePrefix ) - self.DisplayMessagePrefix = DisplayMessagePrefix or "" - return self -end - - ---- Set the scale for scoring valid destroys (enemy destroys). --- A default calculated score is a value between 1 and 10. --- The scale magnifies the scores given to the players. --- @param #SCORING self --- @param #number Scale The scale of the score given. -function SCORING:SetScaleDestroyScore( Scale ) - self.ScaleDestroyScore = Scale - return self -end - ---- Set the scale for scoring penalty destroys (friendly destroys). --- A default calculated penalty is a value between 1 and 10. --- The scale magnifies the scores given to the players. --- @param #SCORING self --- @param #number Scale The scale of the score given. --- @return #SCORING -function SCORING:SetScaleDestroyPenalty( Scale ) - - self.ScaleDestroyPenalty = Scale - - return self -end - ---- Add a @{Unit} for additional scoring when the @{Unit} is destroyed. --- Note that if there was already a @{Unit} declared within the scoring with the same name, --- then the old @{Unit} will be replaced with the new @{Unit}. --- @param #SCORING self --- @param Wrapper.Unit#UNIT ScoreUnit The @{Unit} for which the Score needs to be given. --- @param #number Score The Score value. --- @return #SCORING -function SCORING:AddUnitScore( ScoreUnit, Score ) - - local UnitName = ScoreUnit:GetName() - - self.ScoringObjects[UnitName] = Score - - return self -end - ---- Removes a @{Unit} for additional scoring when the @{Unit} is destroyed. --- @param #SCORING self --- @param Wrapper.Unit#UNIT ScoreUnit The @{Unit} for which the Score needs to be given. --- @return #SCORING -function SCORING:RemoveUnitScore( ScoreUnit ) - - local UnitName = ScoreUnit:GetName() - - self.ScoringObjects[UnitName] = nil - - return self -end - ---- Add a @{Static} for additional scoring when the @{Static} is destroyed. --- Note that if there was already a @{Static} declared within the scoring with the same name, --- then the old @{Static} will be replaced with the new @{Static}. --- @param #SCORING self --- @param Wrapper.Static#UNIT ScoreStatic The @{Static} for which the Score needs to be given. --- @param #number Score The Score value. --- @return #SCORING -function SCORING:AddStaticScore( ScoreStatic, Score ) - - local StaticName = ScoreStatic:GetName() - - self.ScoringObjects[StaticName] = Score - - return self -end - ---- Removes a @{Static} for additional scoring when the @{Static} is destroyed. --- @param #SCORING self --- @param Wrapper.Static#UNIT ScoreStatic The @{Static} for which the Score needs to be given. --- @return #SCORING -function SCORING:RemoveStaticScore( ScoreStatic ) - - local StaticName = ScoreStatic:GetName() - - self.ScoringObjects[StaticName] = nil - - return self -end - - ---- Specify a special additional score for a @{Group}. --- @param #SCORING self --- @param Wrapper.Group#GROUP ScoreGroup The @{Group} for which each @{Unit} a Score is given. --- @param #number Score The Score value. --- @return #SCORING -function SCORING:AddScoreGroup( ScoreGroup, Score ) - - local ScoreUnits = ScoreGroup:GetUnits() - - for ScoreUnitID, ScoreUnit in pairs( ScoreUnits ) do - local UnitName = ScoreUnit:GetName() - self.ScoringObjects[UnitName] = Score - end - - return self -end - ---- Add a @{Zone} to define additional scoring when any object is destroyed in that zone. --- Note that if a @{Zone} with the same name is already within the scoring added, the @{Zone} (type) and Score will be replaced! --- This allows for a dynamic destruction zone evolution within your mission. --- @param #SCORING self --- @param Core.Zone#ZONE_BASE ScoreZone The @{Zone} which defines the destruction score perimeters. --- Note that a zone can be a polygon or a moving zone. --- @param #number Score The Score value. --- @return #SCORING -function SCORING:AddZoneScore( ScoreZone, Score ) - - local ZoneName = ScoreZone:GetName() - - self.ScoringZones[ZoneName] = {} - self.ScoringZones[ZoneName].ScoreZone = ScoreZone - self.ScoringZones[ZoneName].Score = Score - - return self -end - ---- Remove a @{Zone} for additional scoring. --- The scoring will search if any @{Zone} is added with the given name, and will remove that zone from the scoring. --- This allows for a dynamic destruction zone evolution within your mission. --- @param #SCORING self --- @param Core.Zone#ZONE_BASE ScoreZone The @{Zone} which defines the destruction score perimeters. --- Note that a zone can be a polygon or a moving zone. --- @return #SCORING -function SCORING:RemoveZoneScore( ScoreZone ) - - local ZoneName = ScoreZone:GetName() - - self.ScoringZones[ZoneName] = nil - - return self -end - - ---- Configure to send messages after a target has been hit. --- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. --- @return #SCORING -function SCORING:SetMessagesHit( OnOff ) - - self.MessagesHit = OnOff - return self -end - ---- If to send messages after a target has been hit. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesHit() - - return self.MessagesHit -end - ---- Configure to send messages after a target has been destroyed. --- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. --- @return #SCORING -function SCORING:SetMessagesDestroy( OnOff ) - - self.MessagesDestroy = OnOff - return self -end - ---- If to send messages after a target has been destroyed. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesDestroy() - - return self.MessagesDestroy -end - ---- Configure to send messages after a target has been destroyed and receives additional scores. --- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. --- @return #SCORING -function SCORING:SetMessagesScore( OnOff ) - - self.MessagesScore = OnOff - return self -end - ---- If to send messages after a target has been destroyed and receives additional scores. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesScore() - - return self.MessagesScore -end - ---- Configure to send messages after a target has been hit in a zone, and additional score is received. --- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. --- @return #SCORING -function SCORING:SetMessagesZone( OnOff ) - - self.MessagesZone = OnOff - return self -end - ---- If to send messages after a target has been hit in a zone, and additional score is received. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesZone() - - return self.MessagesZone -end - ---- Configure to send messages to all players. --- @param #SCORING self --- @return #SCORING -function SCORING:SetMessagesToAll() - - self.MessagesAudience = 1 - return self -end - ---- If to send messages to all players. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesToAll() - - return self.MessagesAudience == 1 -end - ---- Configure to send messages to only those players within the same coalition as the player. --- @param #SCORING self --- @return #SCORING -function SCORING:SetMessagesToCoalition() - - self.MessagesAudience = 2 - return self -end - ---- If to send messages to only those players within the same coalition as the player. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesToCoalition() - - return self.MessagesAudience == 2 -end - - ---- When a player commits too much damage to friendlies, his penalty score will reach a certain level. --- Use this method to define the level when a player gets kicked. --- By default, the fratricide level is the default penalty mutiplier * 2 for the penalty score. --- @param #SCORING self --- @param #number Fratricide The amount of maximum penalty that may be inflicted by a friendly player before he gets kicked. --- @return #SCORING -function SCORING:SetFratricide( Fratricide ) - - self.Fratricide = Fratricide - return self -end - - ---- When a player changes the coalition, he can receive a penalty score. --- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition. --- By default, the penalty for changing coalition is the default penalty scale. --- @param #SCORING self --- @param #number CoalitionChangePenalty The amount of penalty that is given. --- @return #SCORING -function SCORING:SetCoalitionChangePenalty( CoalitionChangePenalty ) - - self.CoalitionChangePenalty = CoalitionChangePenalty - return self -end - - ---- Add a new player entering a Unit. --- @param #SCORING self --- @param Wrapper.Unit#UNIT UnitData -function SCORING:_AddPlayerFromUnit( UnitData ) - self:F( UnitData ) - - if UnitData:IsAlive() then - local UnitName = UnitData:GetName() - local PlayerName = UnitData:GetPlayerName() - local UnitDesc = UnitData:GetDesc() - local UnitCategory = UnitDesc.category - local UnitCoalition = UnitData:GetCoalition() - local UnitTypeName = UnitData:GetTypeName() - local UnitThreatLevel, UnitThreatType = UnitData:GetThreatLevel() - - self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) - - if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... - self.Players[PlayerName] = {} - self.Players[PlayerName].Hit = {} - self.Players[PlayerName].Destroy = {} - self.Players[PlayerName].Goals = {} - self.Players[PlayerName].Mission = {} - - -- for CategoryID, CategoryName in pairs( SCORINGCategory ) do - -- self.Players[PlayerName].Hit[CategoryID] = {} - -- self.Players[PlayerName].Destroy[CategoryID] = {} - -- end - self.Players[PlayerName].HitPlayers = {} - self.Players[PlayerName].Score = 0 - self.Players[PlayerName].Penalty = 0 - self.Players[PlayerName].PenaltyCoalition = 0 - self.Players[PlayerName].PenaltyWarning = 0 - end - - if not self.Players[PlayerName].UnitCoalition then - self.Players[PlayerName].UnitCoalition = UnitCoalition - else - if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then - self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 - self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 - MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] .. - "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", - MESSAGE.Type.Information - ):ToAll() - self:ScoreCSV( PlayerName, "", "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, - UnitName, _SCORINGCoalition[UnitCoalition], _SCORINGCategory[UnitCategory], UnitData:GetTypeName() ) - end - end - self.Players[PlayerName].UnitName = UnitName - self.Players[PlayerName].UnitCoalition = UnitCoalition - self.Players[PlayerName].UnitCategory = UnitCategory - self.Players[PlayerName].UnitType = UnitTypeName - self.Players[PlayerName].UNIT = UnitData - self.Players[PlayerName].ThreatLevel = UnitThreatLevel - self.Players[PlayerName].ThreatType = UnitThreatType - - if self.Players[PlayerName].Penalty > self.Fratricide * 0.50 then - if self.Players[PlayerName].PenaltyWarning < 1 then - MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than " .. self.Fratricide .. ", you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, - MESSAGE.Type.Information - ):ToAll() - self.Players[PlayerName].PenaltyWarning = self.Players[PlayerName].PenaltyWarning + 1 - end - end - - if self.Players[PlayerName].Penalty > self.Fratricide then - MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", - MESSAGE.Type.Information - ):ToAll() - UnitData:GetGroup():Destroy() - end - - end -end - - ---- Add a goal score for a player. --- The method takes the PlayerUnit for which the Goal score needs to be set. --- The GoalTag is a string or identifier that is taken into the CSV file scoring log to identify the goal. --- A free text can be given that is shown to the players. --- The Score can be both positive and negative. --- @param #SCORING self --- @param Wrapper.Unit#UNIT PlayerUnit The @{Unit} of the Player. Other Properties for the scoring are taken from this PlayerUnit, like coalition, type etc. --- @param #string GoalTag The string or identifier that is used in the CSV file to identify the goal (sort or group later in Excel). --- @param #string Text A free text that is shown to the players. --- @param #number Score The score can be both positive or negative ( Penalty ). -function SCORING:AddGoalScore( PlayerUnit, GoalTag, Text, Score ) - - local PlayerName = PlayerUnit:GetPlayerName() - - self:E( { PlayerUnit.UnitName, PlayerName, GoalTag, Text, Score } ) - - -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. - if PlayerName then - local PlayerData = self.Players[PlayerName] - - PlayerData.Goals[GoalTag] = PlayerData.Goals[GoalTag] or { Score = 0 } - PlayerData.Goals[GoalTag].Score = PlayerData.Goals[GoalTag].Score + Score - PlayerData.Score = PlayerData.Score + Score - - MESSAGE:NewType( self.DisplayMessagePrefix .. Text, MESSAGE.Type.Information ):ToAll() - - self:ScoreCSV( PlayerName, "", "GOAL_" .. string.upper( GoalTag ), 1, Score, PlayerUnit:GetName() ) - end -end - - ---- Registers Scores the players completing a Mission Task. --- @param #SCORING self --- @param Tasking.Mission#MISSION Mission --- @param Wrapper.Unit#UNIT PlayerUnit --- @param #string Text --- @param #number Score -function SCORING:_AddMissionTaskScore( Mission, PlayerUnit, Text, Score ) - - local PlayerName = PlayerUnit:GetPlayerName() - local MissionName = Mission:GetName() - - self:E( { Mission:GetName(), PlayerUnit.UnitName, PlayerName, Text, Score } ) - - -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. - if PlayerName then - local PlayerData = self.Players[PlayerName] - - if not PlayerData.Mission[MissionName] then - PlayerData.Mission[MissionName] = {} - PlayerData.Mission[MissionName].ScoreTask = 0 - PlayerData.Mission[MissionName].ScoreMission = 0 - end - - self:T( PlayerName ) - self:T( PlayerData.Mission[MissionName] ) - - PlayerData.Score = self.Players[PlayerName].Score + Score - PlayerData.Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score - - MESSAGE:NewType( self.DisplayMessagePrefix .. MissionName .. " : " .. Text .. " Score: " .. Score, MESSAGE.Type.Information ):ToAll() - - self:ScoreCSV( PlayerName, "", "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score, PlayerUnit:GetName() ) - end -end - - ---- Registers Mission Scores for possible multiple players that contributed in the Mission. --- @param #SCORING self --- @param Tasking.Mission#MISSION Mission --- @param Wrapper.Unit#UNIT PlayerUnit --- @param #string Text --- @param #number Score -function SCORING:_AddMissionScore( Mission, Text, Score ) - - local MissionName = Mission:GetName() - - self:E( { Mission, Text, Score } ) - self:E( self.Players ) - - for PlayerName, PlayerData in pairs( self.Players ) do - - self:E( PlayerData ) - if PlayerData.Mission[MissionName] then - - PlayerData.Score = PlayerData.Score + Score - PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score - - MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' has " .. Text .. " in Mission '" .. MissionName .. "'. " .. - Score .. " mission score!", - MESSAGE.Type.Information ):ToAll() - - self:ScoreCSV( PlayerName, "", "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score ) - end - end -end - - ---- Handles the OnPlayerEnterUnit event for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:OnEventPlayerEnterUnit( Event ) - if Event.IniUnit then - self:_AddPlayerFromUnit( Event.IniUnit ) - local Menu = MENU_GROUP:New( Event.IniGroup, 'Scoring' ) - local ReportGroupSummary = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Summary report players in group', Menu, SCORING.ReportScoreGroupSummary, self, Event.IniGroup ) - local ReportGroupDetailed = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Detailed report players in group', Menu, SCORING.ReportScoreGroupDetailed, self, Event.IniGroup ) - local ReportToAllSummary = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Summary report all players', Menu, SCORING.ReportScoreAllSummary, self, Event.IniGroup ) - self:SetState( Event.IniUnit, "ScoringMenu", Menu ) - end -end - ---- Handles the OnPlayerLeaveUnit event for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:OnEventPlayerLeaveUnit( Event ) - if Event.IniUnit then - local Menu = self:GetState( Event.IniUnit, "ScoringMenu" ) -- Core.Menu#MENU_GROUP - if Menu then - -- TODO: Check if this fixes #281. - --Menu:Remove() - end - end -end - - ---- Handles the OnHit event for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:_EventOnHit( Event ) - self:F( { Event } ) - - local InitUnit = nil - local InitUNIT = nil - local InitUnitName = "" - local InitGroup = nil - local InitGroupName = "" - local InitPlayerName = nil - - local InitCoalition = nil - local InitCategory = nil - local InitType = nil - local InitUnitCoalition = nil - local InitUnitCategory = nil - local InitUnitType = nil - - local TargetUnit = nil - local TargetUNIT = nil - local TargetUnitName = "" - local TargetGroup = nil - local TargetGroupName = "" - local TargetPlayerName = nil - - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil - - if Event.IniDCSUnit then - - InitUnit = Event.IniDCSUnit - InitUNIT = Event.IniUnit - InitUnitName = Event.IniDCSUnitName - InitGroup = Event.IniDCSGroup - InitGroupName = Event.IniDCSGroupName - InitPlayerName = Event.IniPlayerName - - InitCoalition = Event.IniCoalition - --TODO: Workaround Client DCS Bug - --InitCategory = InitUnit:getCategory() - --InitCategory = InitUnit:getDesc().category - InitCategory = Event.IniCategory - InitType = Event.IniTypeName - - InitUnitCoalition = _SCORINGCoalition[InitCoalition] - InitUnitCategory = _SCORINGCategory[InitCategory] - InitUnitType = InitType - - self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) - end - - - if Event.TgtDCSUnit then - - TargetUnit = Event.TgtDCSUnit - TargetUNIT = Event.TgtUnit - TargetUnitName = Event.TgtDCSUnitName - TargetGroup = Event.TgtDCSGroup - TargetGroupName = Event.TgtDCSGroupName - TargetPlayerName = Event.TgtPlayerName - - TargetCoalition = Event.TgtCoalition - --TODO: Workaround Client DCS Bug - --TargetCategory = TargetUnit:getCategory() - --TargetCategory = TargetUnit:getDesc().category - TargetCategory = Event.TgtCategory - TargetType = Event.TgtTypeName - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) - end - - if InitPlayerName ~= nil then -- It is a player that is hitting something - self:_AddPlayerFromUnit( InitUNIT ) - if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - self:_AddPlayerFromUnit( TargetUNIT ) - end - - self:T( "Hitting Something" ) - - -- What is he hitting? - if TargetCategory then - - -- A target got hit, score it. - -- Player contains the score data from self.Players[InitPlayerName] - local Player = self.Players[InitPlayerName] - - -- Ensure there is a hit table per TargetCategory and TargetUnitName. - Player.Hit[TargetCategory] = Player.Hit[TargetCategory] or {} - Player.Hit[TargetCategory][TargetUnitName] = Player.Hit[TargetCategory][TargetUnitName] or {} - - -- PlayerHit contains the score counters and data per unit that was hit. - local PlayerHit = Player.Hit[TargetCategory][TargetUnitName] - - PlayerHit.Score = PlayerHit.Score or 0 - PlayerHit.Penalty = PlayerHit.Penalty or 0 - PlayerHit.ScoreHit = PlayerHit.ScoreHit or 0 - PlayerHit.PenaltyHit = PlayerHit.PenaltyHit or 0 - PlayerHit.TimeStamp = PlayerHit.TimeStamp or 0 - PlayerHit.UNIT = PlayerHit.UNIT or TargetUNIT - PlayerHit.ThreatLevel, PlayerHit.ThreatType = PlayerHit.UNIT:GetThreatLevel() - - -- Only grant hit scores if there was more than one second between the last hit. - if timer.getTime() - PlayerHit.TimeStamp > 1 then - PlayerHit.TimeStamp = timer.getTime() - - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - - -- Ensure there is a Player to Player hit reference table. - Player.HitPlayers[TargetPlayerName] = true - end - - local Score = 0 - - if InitCoalition then -- A coalition object was hit. - if InitCoalition == TargetCoalition then - Player.Penalty = Player.Penalty + 10 - PlayerHit.Penalty = PlayerHit.Penalty + 10 - PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1 - - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit friendly player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. - "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - MESSAGE.Type.Update - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - else - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit friendly target " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. - "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - MESSAGE.Type.Update - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - end - self:ScoreCSV( InitPlayerName, TargetPlayerName, "HIT_PENALTY", 1, -10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - Player.Score = Player.Score + 1 - PlayerHit.Score = PlayerHit.Score + 1 - PlayerHit.ScoreHit = PlayerHit.ScoreHit + 1 - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit enemy player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. - "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, - MESSAGE.Type.Update - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - else - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit enemy target " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. - "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, - MESSAGE.Type.Update - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - end - self:ScoreCSV( InitPlayerName, TargetPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - else -- A scenery object was hit. - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit scenery object.", - MESSAGE.Type.Update - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - self:ScoreCSV( InitPlayerName, "", "HIT_SCORE", 1, 0, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) - end - end - end - end - elseif InitPlayerName == nil then -- It is an AI hitting a player??? - - end - - -- It is a weapon initiated by a player, that is hitting something - -- This seems to occur only with scenery and static objects. - if Event.WeaponPlayerName ~= nil then - self:_AddPlayerFromUnit( Event.WeaponUNIT ) - if self.Players[Event.WeaponPlayerName] then -- This should normally not happen, but i'll test it anyway. - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - self:_AddPlayerFromUnit( TargetUNIT ) - end - - self:T( "Hitting Scenery" ) - - -- What is he hitting? - if TargetCategory then - - -- A scenery or static got hit, score it. - -- Player contains the score data from self.Players[WeaponPlayerName] - local Player = self.Players[Event.WeaponPlayerName] - - -- Ensure there is a hit table per TargetCategory and TargetUnitName. - Player.Hit[TargetCategory] = Player.Hit[TargetCategory] or {} - Player.Hit[TargetCategory][TargetUnitName] = Player.Hit[TargetCategory][TargetUnitName] or {} - - -- PlayerHit contains the score counters and data per unit that was hit. - local PlayerHit = Player.Hit[TargetCategory][TargetUnitName] - - PlayerHit.Score = PlayerHit.Score or 0 - PlayerHit.Penalty = PlayerHit.Penalty or 0 - PlayerHit.ScoreHit = PlayerHit.ScoreHit or 0 - PlayerHit.PenaltyHit = PlayerHit.PenaltyHit or 0 - PlayerHit.TimeStamp = PlayerHit.TimeStamp or 0 - PlayerHit.UNIT = PlayerHit.UNIT or TargetUNIT - PlayerHit.ThreatLevel, PlayerHit.ThreatType = PlayerHit.UNIT:GetThreatLevel() - - -- Only grant hit scores if there was more than one second between the last hit. - if timer.getTime() - PlayerHit.TimeStamp > 1 then - PlayerHit.TimeStamp = timer.getTime() - - local Score = 0 - - if InitCoalition then -- A coalition object was hit, probably a static. - if InitCoalition == TargetCoalition then - -- TODO: Penalty according scale - Player.Penalty = Player.Penalty + 10 - PlayerHit.Penalty = PlayerHit.Penalty + 10 - PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1 - - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. Event.WeaponPlayerName .. "' hit friendly target " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - "Penalty: -" .. PlayerHit.Penalty .. " = " .. Player.Score - Player.Penalty, - MESSAGE.Type.Update - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( Event.WeaponCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - self:ScoreCSV( Event.WeaponPlayerName, TargetPlayerName, "HIT_PENALTY", 1, -10, Event.WeaponName, Event.WeaponCoalition, Event.WeaponCategory, Event.WeaponTypeName, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - Player.Score = Player.Score + 1 - PlayerHit.Score = PlayerHit.Score + 1 - PlayerHit.ScoreHit = PlayerHit.ScoreHit + 1 - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. Event.WeaponPlayerName .. "' hit enemy target " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - "Score: +" .. PlayerHit.Score .. " = " .. Player.Score - Player.Penalty, - MESSAGE.Type.Update - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( Event.WeaponCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - self:ScoreCSV( Event.WeaponPlayerName, TargetPlayerName, "HIT_SCORE", 1, 1, Event.WeaponName, Event.WeaponCoalition, Event.WeaponCategory, Event.WeaponTypeName, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - else -- A scenery object was hit. - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. Event.WeaponPlayerName .. "' hit scenery object.", - MESSAGE.Type.Update - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - self:ScoreCSV( Event.WeaponPlayerName, "", "HIT_SCORE", 1, 0, Event.WeaponName, Event.WeaponCoalition, Event.WeaponCategory, Event.WeaponTypeName, TargetUnitName, "", "Scenery", TargetUnitType ) - end - end - end - end - end -end - ---- Track DEAD or CRASH events for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:_EventOnDeadOrCrash( Event ) - self:F( { Event } ) - - local TargetUnit = nil - local TargetGroup = nil - local TargetUnitName = "" - local TargetGroupName = "" - local TargetPlayerName = "" - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil - - if Event.IniDCSUnit then - - TargetUnit = Event.IniUnit - TargetUnitName = Event.IniDCSUnitName - TargetGroup = Event.IniDCSGroup - TargetGroupName = Event.IniDCSGroupName - TargetPlayerName = Event.IniPlayerName - - TargetCoalition = Event.IniCoalition - --TargetCategory = TargetUnit:getCategory() - --TargetCategory = TargetUnit:getDesc().category -- Workaround - TargetCategory = Event.IniCategory - TargetType = Event.IniTypeName - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) - end - - -- Player contains the score and reference data for the player. - for PlayerName, Player in pairs( self.Players ) do - if Player then -- This should normally not happen, but i'll test it anyway. - self:T( "Something got destroyed" ) - - -- Some variables - local InitUnitName = Player.UnitName - local InitUnitType = Player.UnitType - local InitCoalition = Player.UnitCoalition - local InitCategory = Player.UnitCategory - local InitUnitCoalition = _SCORINGCoalition[InitCoalition] - local InitUnitCategory = _SCORINGCategory[InitCategory] - - self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) - - local Destroyed = false - - -- What is the player destroying? - if Player and Player.Hit and Player.Hit[TargetCategory] and Player.Hit[TargetCategory][TargetUnitName] and Player.Hit[TargetCategory][TargetUnitName].TimeStamp ~= 0 then -- Was there a hit for this unit for this player before registered??? - - local TargetThreatLevel = Player.Hit[TargetCategory][TargetUnitName].ThreatLevel - local TargetThreatType = Player.Hit[TargetCategory][TargetUnitName].ThreatType - - Player.Destroy[TargetCategory] = Player.Destroy[TargetCategory] or {} - Player.Destroy[TargetCategory][TargetType] = Player.Destroy[TargetCategory][TargetType] or {} - - -- PlayerDestroy contains the destroy score data per category and target type of the player. - local TargetDestroy = Player.Destroy[TargetCategory][TargetType] - TargetDestroy.Score = TargetDestroy.Score or 0 - TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy or 0 - TargetDestroy.Penalty = TargetDestroy.Penalty or 0 - TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy or 0 - - if TargetCoalition then - if InitCoalition == TargetCoalition then - local ThreatLevelTarget = TargetThreatLevel - local ThreatTypeTarget = TargetThreatType - local ThreatLevelPlayer = Player.ThreatLevel / 10 + 1 - local ThreatPenalty = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyPenalty / 10 ) - self:E( { ThreatLevel = ThreatPenalty, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) - - Player.Penalty = Player.Penalty + ThreatPenalty - TargetDestroy.Penalty = TargetDestroy.Penalty + ThreatPenalty - TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy + 1 - - if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed friendly player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. - "Penalty: -" .. TargetDestroy.Penalty .. " = " .. Player.Score - Player.Penalty, - MESSAGE.Type.Information - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) - else - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed friendly target " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. - "Penalty: -" .. TargetDestroy.Penalty .. " = " .. Player.Score - Player.Penalty, - MESSAGE.Type.Information - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) - end - - Destroyed = true - self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_PENALTY", 1, ThreatPenalty, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - - local ThreatLevelTarget = TargetThreatLevel - local ThreatTypeTarget = TargetThreatType - local ThreatLevelPlayer = Player.ThreatLevel / 10 + 1 - local ThreatScore = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyScore / 10 ) - - self:E( { ThreatLevel = ThreatScore, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) - - Player.Score = Player.Score + ThreatScore - TargetDestroy.Score = TargetDestroy.Score + ThreatScore - TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy + 1 - if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed enemy player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. - "Score: +" .. TargetDestroy.Score .. " = " .. Player.Score - Player.Penalty, - MESSAGE.Type.Information - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) - else - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed enemy " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. - "Score: +" .. TargetDestroy.Score .. " = " .. Player.Score - Player.Penalty, - MESSAGE.Type.Information - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) - end - Destroyed = true - self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, ThreatScore, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - - local UnitName = TargetUnit:GetName() - local Score = self.ScoringObjects[UnitName] - if Score then - Player.Score = Player.Score + Score - TargetDestroy.Score = TargetDestroy.Score + Score - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Special target '" .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. " destroyed! " .. - "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! Total: " .. Player.Score - Player.Penalty, - MESSAGE.Type.Information - ) - :ToAllIf( self:IfMessagesScore() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesScore() and self:IfMessagesToCoalition() ) - self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - Destroyed = true - end - - -- Check if there are Zones where the destruction happened. - for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do - self:E( { ScoringZone = ScoreZoneData } ) - local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE - local Score = ScoreZoneData.Score - if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then - Player.Score = Player.Score + Score - TargetDestroy.Score = TargetDestroy.Score + Score - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Target destroyed in zone '" .. ScoreZone:GetName() .. "'." .. - "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. - "Total: " .. Player.Score - Player.Penalty, - MESSAGE.Type.Information ) - :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) - self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - Destroyed = true - end - end - - end - else - -- Check if there are Zones where the destruction happened. - for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do - self:E( { ScoringZone = ScoreZoneData } ) - local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE - local Score = ScoreZoneData.Score - if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then - Player.Score = Player.Score + Score - TargetDestroy.Score = TargetDestroy.Score + Score - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Scenery destroyed in zone '" .. ScoreZone:GetName() .. "'." .. - "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. - "Total: " .. Player.Score - Player.Penalty, - MESSAGE.Type.Information - ) - :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) - Destroyed = true - self:ScoreCSV( PlayerName, "", "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) - end - end - end - - -- Delete now the hit cache if the target was destroyed. - -- Otherwise points will be granted every time a target gets killed by the players that hit that target. - -- This is only relevant for player to player destroys. - if Destroyed then - Player.Hit[TargetCategory][TargetUnitName].TimeStamp = 0 - end - end - end - end -end - - ---- Produce detailed report of player hit scores. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerHits( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageHits = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - self:T( "Hit scores exist for player " .. PlayerName ) - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = "Hits: " .. ScoreMessageHits - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - - ---- Produce detailed report of player destroy scores. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerDestroys( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageDestroys = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - if PlayerData.Destroy[CategoryID] then - self:T( "Destroy scores exist for player " .. PlayerName ) - local Score = 0 - local ScoreDestroy = 0 - local Penalty = 0 - local PenaltyDestroy = 0 - - for UnitName, UnitData in pairs( PlayerData.Destroy[CategoryID] ) do - self:E( { UnitData = UnitData } ) - if UnitData ~= {} then - Score = Score + UnitData.Score - ScoreDestroy = ScoreDestroy + UnitData.ScoreDestroy - Penalty = Penalty + UnitData.Penalty - PenaltyDestroy = PenaltyDestroy + UnitData.PenaltyDestroy - end - end - - local ScoreMessageDestroy = string.format( " %s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageDestroy ) - ScoreMessageDestroys = ScoreMessageDestroys .. ScoreMessageDestroy - - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageDestroys = ScoreMessageDestroys .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageDestroys ~= "" then - ScoreMessage = "Destroys: " .. ScoreMessageDestroys - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - ---- Produce detailed report of player penalty scores because of changing the coalition. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerCoalitionChanges( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. "Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - ---- Produce detailed report of player goal scores. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerGoals( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageGoal = "" - local ScoreGoal = 0 - local ScoreTask = 0 - for GoalName, GoalData in pairs( PlayerData.Goals ) do - ScoreGoal = ScoreGoal + GoalData.Score - ScoreMessageGoal = ScoreMessageGoal .. "'" .. GoalName .. "':" .. GoalData.Score .. "; " - end - PlayerScore = PlayerScore + ScoreGoal - - if ScoreMessageGoal ~= "" then - ScoreMessage = "Goals: " .. ScoreMessageGoal - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - ---- Produce detailed report of player penalty scores because of changing the coalition. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerMissions( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask - - if ScoreMessageMission ~= "" then - ScoreMessage = "Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")" - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - - ---- Report Group Score Summary --- @param #SCORING self --- @param Wrapper.Group#GROUP PlayerGroup The player group. -function SCORING:ReportScoreGroupSummary( PlayerGroup ) - - local PlayerMessage = "" - - self:T( "Report Score Group Summary" ) - - local PlayerUnits = PlayerGroup:GetUnits() - for UnitID, PlayerUnit in pairs( PlayerUnits ) do - local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT - local PlayerName = PlayerUnit:GetPlayerName() - - if PlayerName then - - local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) - ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits - self:E( { ReportHits, ScoreHits, PenaltyHits } ) - - local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) - ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys - self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) - - local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) - ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges - self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) - - local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) - ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals - self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) - - local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) - ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions - self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) - - local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions - - PlayerMessage = - string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", - PlayerName, - PlayerScore - PlayerPenalty, - PlayerScore, - PlayerPenalty - ) - MESSAGE:NewType( PlayerMessage, MESSAGE.Type.Detailed ):ToGroup( PlayerGroup ) - end - end - -end - ---- Report Group Score Detailed --- @param #SCORING self --- @param Wrapper.Group#GROUP PlayerGroup The player group. -function SCORING:ReportScoreGroupDetailed( PlayerGroup ) - - local PlayerMessage = "" - - self:T( "Report Score Group Detailed" ) - - local PlayerUnits = PlayerGroup:GetUnits() - for UnitID, PlayerUnit in pairs( PlayerUnits ) do - local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT - local PlayerName = PlayerUnit:GetPlayerName() - - if PlayerName then - - local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) - ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits - self:E( { ReportHits, ScoreHits, PenaltyHits } ) - - local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) - ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys - self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) - - local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) - ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges - self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) - - local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) - ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals - self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) - - local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) - ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions - self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) - - local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions - - PlayerMessage = - string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )%s%s%s%s%s", - PlayerName, - PlayerScore - PlayerPenalty, - PlayerScore, - PlayerPenalty, - ReportHits, - ReportDestroys, - ReportCoalitionChanges, - ReportGoals, - ReportMissions - ) - MESSAGE:NewType( PlayerMessage, MESSAGE.Type.Detailed ):ToGroup( PlayerGroup ) - end - end - -end - ---- Report all players score --- @param #SCORING self --- @param Wrapper.Group#GROUP PlayerGroup The player group. -function SCORING:ReportScoreAllSummary( PlayerGroup ) - - local PlayerMessage = "" - - self:T( "Report Score All Players" ) - - for PlayerName, PlayerData in pairs( self.Players ) do - - if PlayerName then - - local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) - ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits - self:E( { ReportHits, ScoreHits, PenaltyHits } ) - - local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) - ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys - self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) - - local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) - ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges - self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) - - local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) - ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals - self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) - - local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) - ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions - self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) - - local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions - - PlayerMessage = - string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", - PlayerName, - PlayerScore - PlayerPenalty, - PlayerScore, - PlayerPenalty - ) - MESSAGE:NewType( PlayerMessage, MESSAGE.Type.Overview ):ToGroup( PlayerGroup ) - end - end - -end - - -function SCORING:SecondsToClock(sSeconds) - local nSeconds = sSeconds - if nSeconds == 0 then - --return nil; - return "00:00:00"; - else - nHours = string.format("%02.f", math.floor(nSeconds/3600)); - nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); - nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); - return nHours..":"..nMins..":"..nSecs - end -end - ---- Opens a score CSV file to log the scores. --- @param #SCORING self --- @param #string ScoringCSV --- @return #SCORING self --- @usage --- -- Open a new CSV file to log the scores of the game Gori Valley. Let the name of the CSV file begin with "Player Scores". --- ScoringObject = SCORING:New( "Gori Valley" ) --- ScoringObject:OpenCSV( "Player Scores" ) -function SCORING:OpenCSV( ScoringCSV ) - self:F( ScoringCSV ) - - if lfs and io and os then - if ScoringCSV then - self.ScoringCSV = ScoringCSV - local fdir = lfs.writedir() .. [[Logs\]] .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv" - - self.CSVFile, self.err = io.open( fdir, "w+" ) - if not self.CSVFile then - error( "Error: Cannot open CSV file in " .. lfs.writedir() ) - end - - self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","TargetPlayerName","ScoreType","PlayerUnitCoaltion","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n' ) - - self.RunTime = os.date("%y-%m-%d_%H-%M-%S") - else - error( "A string containing the CSV file name must be given." ) - end - else - self:E( "The MissionScripting.lua file has not been changed to allow lfs, io and os modules to be used..." ) - end - return self -end - - ---- Registers a score for a player. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @param #string TargetPlayerName The name of the target player. --- @param #string ScoreType The type of the score. --- @param #string ScoreTimes The amount of scores achieved. --- @param #string ScoreAmount The score given. --- @param #string PlayerUnitName The unit name of the player. --- @param #string PlayerUnitCoalition The coalition of the player unit. --- @param #string PlayerUnitCategory The category of the player unit. --- @param #string PlayerUnitType The type of the player unit. --- @param #string TargetUnitName The name of the target unit. --- @param #string TargetUnitCoalition The coalition of the target unit. --- @param #string TargetUnitCategory The category of the target unit. --- @param #string TargetUnitType The type of the target unit. --- @return #SCORING self -function SCORING:ScoreCSV( PlayerName, TargetPlayerName, ScoreType, ScoreTimes, ScoreAmount, PlayerUnitName, PlayerUnitCoalition, PlayerUnitCategory, PlayerUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - --write statistic information to file - local ScoreTime = self:SecondsToClock( timer.getTime() ) - PlayerName = PlayerName:gsub( '"', '_' ) - - TargetPlayerName = TargetPlayerName or "" - TargetPlayerName = TargetPlayerName:gsub( '"', '_' ) - - if PlayerUnitName and PlayerUnitName ~= '' then - local PlayerUnit = Unit.getByName( PlayerUnitName ) - - if PlayerUnit then - if not PlayerUnitCategory then - --PlayerUnitCategory = SCORINGCategory[PlayerUnit:getCategory()] - PlayerUnitCategory = _SCORINGCategory[PlayerUnit:getDesc().category] - end - - if not PlayerUnitCoalition then - PlayerUnitCoalition = _SCORINGCoalition[PlayerUnit:getCoalition()] - end - - if not PlayerUnitType then - PlayerUnitType = PlayerUnit:getTypeName() - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - - TargetUnitCoalition = TargetUnitCoalition or "" - TargetUnitCategory = TargetUnitCategory or "" - TargetUnitType = TargetUnitType or "" - TargetUnitName = TargetUnitName or "" - - if lfs and io and os then - self.CSVFile:write( - '"' .. self.GameName .. '"' .. ',' .. - '"' .. self.RunTime .. '"' .. ',' .. - '' .. ScoreTime .. '' .. ',' .. - '"' .. PlayerName .. '"' .. ',' .. - '"' .. TargetPlayerName .. '"' .. ',' .. - '"' .. ScoreType .. '"' .. ',' .. - '"' .. PlayerUnitCoalition .. '"' .. ',' .. - '"' .. PlayerUnitCategory .. '"' .. ',' .. - '"' .. PlayerUnitType .. '"' .. ',' .. - '"' .. PlayerUnitName .. '"' .. ',' .. - '"' .. TargetUnitCoalition .. '"' .. ',' .. - '"' .. TargetUnitCategory .. '"' .. ',' .. - '"' .. TargetUnitType .. '"' .. ',' .. - '"' .. TargetUnitName .. '"' .. ',' .. - '' .. ScoreTimes .. '' .. ',' .. - '' .. ScoreAmount - ) - - self.CSVFile:write( "\n" ) - end -end - - -function SCORING:CloseCSV() - if lfs and io and os then - self.CSVFile:close() - end -end - ---- **Functional** -- The CLEANUP_AIRBASE class keeps an area clean of crashing or colliding airplanes. It also prevents airplanes from firing within this area. --- --- === --- --- ### Author: **Sven Van de Velde (FlightControl)** --- ### Contributions: --- --- ==== --- --- @module CleanUp - ---- @type CLEANUP_AIRBASE.__ Methods which are not intended for mission designers, but which are used interally by the moose designer :-) --- @field #map<#string,Wrapper.Airbase#AIRBASE> Airbases Map of Airbases. --- @extends Core.Base#BASE - ---- @type CLEANUP_AIRBASE --- @extends #CLEANUP_AIRBASE.__ - ---- # CLEANUP_AIRBASE, extends @{Base#BASE} --- --- ![Banner Image](..\Presentations\CLEANUP_AIRBASE\Dia1.JPG) --- --- The CLEANUP_AIRBASE class keeps airbases clean, and tries to guarantee continuous airbase operations, even under combat. --- Specific airbases need to be provided that need to be guarded. Each airbase registered, will be guarded within a zone of 8 km around the airbase. --- Any unit that fires a missile, or shoots within the zone of an airbase, will be monitored by CLEANUP_AIRBASE. --- Within the 8km zone, units cannot fire any missile, which prevents the airbase runway to receive missile or bomb hits. --- Any airborne or ground unit that is on the runway below 30 meters (default value) will be automatically removed if it is damaged. --- --- This is not a full 100% secure implementation. It is still possible that CLEANUP_AIRBASE cannot prevent (in-time) to keep the airbase clean. --- The following situations may happen that will still stop the runway of an airbase: --- --- * A damaged unit is not removed on time when above the runway, and crashes on the runway. --- * A bomb or missile is still able to dropped on the runway. --- * Units collide on the airbase, and could not be removed on time. --- --- When a unit is within the airbase zone and needs to be monitored, --- its status will be checked every 0.25 seconds! This is required to ensure that the airbase is kept clean. --- But as a result, there is more CPU overload. --- --- So as an advise, I suggest you use the CLEANUP_AIRBASE class with care: --- --- * Only monitor airbases that really need to be monitored! --- * Try not to monitor airbases that are likely to be invaded by enemy troops. --- For these airbases, there is little use to keep them clean, as they will be invaded anyway... --- --- By following the above guidelines, you can add airbase cleanup with acceptable CPU overhead. --- --- ## 1. CLEANUP_AIRBASE Constructor --- --- Creates the main object which is preventing the airbase to get polluted with debris on the runway, which halts the airbase. --- --- -- Clean these Zones. --- CleanUpAirports = CLEANUP_AIRBASE:New( { AIRBASE.Caucasus.Tbilisi, AIRBASE.Caucasus.Kutaisi ) --- --- -- or --- CleanUpTbilisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Tbilisi ) --- CleanUpKutaisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Kutaisi ) --- --- ## 2. Add or Remove airbases --- --- The method @{#CLEANUP_AIRBASE.AddAirbase}() to add an airbase to the cleanup validation process. --- The method @{#CLEANUP_AIRBASE.RemoveAirbase}() removes an airbase from the cleanup validation process. --- --- ## 3. Clean missiles and bombs within the airbase zone. --- --- When missiles or bombs hit the runway, the airbase operations stop. --- Use the method @{#CLEANUP_AIRBASE.SetCleanMissiles}() to control the cleaning of missiles, which will prevent airbases to stop. --- Note that this method will not allow anymore airbases to be attacked, so there is a trade-off here to do. --- --- @field #CLEANUP_AIRBASE -CLEANUP_AIRBASE = { - ClassName = "CLEANUP_AIRBASE", - TimeInterval = 0.2, - CleanUpList = {}, -} - --- @field #CLEANUP_AIRBASE.__ -CLEANUP_AIRBASE.__ = {} - ---- @field #CLEANUP_AIRBASE.__.Airbases -CLEANUP_AIRBASE.__.Airbases = {} - ---- Creates the main object which is handling the cleaning of the debris within the given Zone Names. --- @param #CLEANUP_AIRBASE self --- @param #list<#string> AirbaseNames Is a table of airbase names where the debris should be cleaned. Also a single string can be passed with one airbase name. --- @return #CLEANUP_AIRBASE --- @usage --- -- Clean these Zones. --- CleanUpAirports = CLEANUP_AIRBASE:New( { AIRBASE.Caucasus.Tbilisi, AIRBASE.Caucasus.Kutaisi ) --- or --- CleanUpTbilisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Tbilisi ) --- CleanUpKutaisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Kutaisi ) -function CLEANUP_AIRBASE:New( AirbaseNames ) - - local self = BASE:Inherit( self, BASE:New() ) -- #CLEANUP_AIRBASE - self:F( { AirbaseNames } ) - - if type( AirbaseNames ) == 'table' then - for AirbaseID, AirbaseName in pairs( AirbaseNames ) do - self:AddAirbase( AirbaseName ) - end - else - local AirbaseName = AirbaseNames - self:AddAirbase( AirbaseName ) - end - - self:HandleEvent( EVENTS.Birth, self.__.OnEventBirth ) - - self.__.CleanUpScheduler = SCHEDULER:New( self, self.__.CleanUpSchedule, {}, 1, self.TimeInterval ) - - self:HandleEvent( EVENTS.EngineShutdown , self.__.EventAddForCleanUp ) - self:HandleEvent( EVENTS.EngineStartup, self.__.EventAddForCleanUp ) - self:HandleEvent( EVENTS.Hit, self.__.EventAddForCleanUp ) - self:HandleEvent( EVENTS.PilotDead, self.__.OnEventCrash ) - self:HandleEvent( EVENTS.Dead, self.__.OnEventCrash ) - self:HandleEvent( EVENTS.Crash, self.__.OnEventCrash ) - - return self -end - ---- Adds an airbase to the airbase validation list. --- @param #CLEANUP_AIRBASE self --- @param #string AirbaseName --- @return #CLEANUP_AIRBASE -function CLEANUP_AIRBASE:AddAirbase( AirbaseName ) - self.__.Airbases[AirbaseName] = AIRBASE:FindByName( AirbaseName ) - self:F({"Airbase:", AirbaseName, self.__.Airbases[AirbaseName]:GetDesc()}) - - return self -end - ---- Removes an airbase from the airbase validation list. --- @param #CLEANUP_AIRBASE self --- @param #string AirbaseName --- @return #CLEANUP_AIRBASE -function CLEANUP_AIRBASE:RemoveAirbase( AirbaseName ) - self.__.Airbases[AirbaseName] = nil - return self -end - ---- Enables or disables the cleaning of missiles within the airbase zones. --- Airbase operations stop when a missile or bomb is dropped at a runway. --- Note that when this method is used, the airbase operations won't stop if --- the missile or bomb was cleaned within the airbase zone, which is 8km from the center of the airbase. --- However, there is a trade-off to make. Attacks on airbases won't be possible anymore if this method is used. --- Note, one can also use the method @{#CLEANUP_AIRBASE.RemoveAirbase}() to remove the airbase from the control process as a whole, --- when an enemy unit is near. That is also an option... --- @param #CLEANUP_AIRBASE self --- @param #string CleanMissiles (Default=true) If true, missiles fired are immediately destroyed. If false missiles are not controlled. --- @return #CLEANUP_AIRBASE -function CLEANUP_AIRBASE:SetCleanMissiles( CleanMissiles ) - - if CleanMissiles then - self:HandleEvent( EVENTS.Shot, self.__.OnEventShot ) - else - self:UnHandleEvent( EVENTS.Shot ) - end -end - -function CLEANUP_AIRBASE.__:IsInAirbase( Vec2 ) - - local InAirbase = false - for AirbaseName, Airbase in pairs( self.__.Airbases ) do - local Airbase = Airbase -- Wrapper.Airbase#AIRBASE - if Airbase:GetZone():IsVec2InZone( Vec2 ) then - InAirbase = true - break; - end - end - - return InAirbase -end - - - ---- Destroys a @{Unit} from the simulator, but checks first if it is still existing! --- @param #CLEANUP_AIRBASE self --- @param Wrapper.Unit#UNIT CleanUpUnit The object to be destroyed. -function CLEANUP_AIRBASE.__:DestroyUnit( CleanUpUnit ) - self:F( { CleanUpUnit } ) - - if CleanUpUnit then - local CleanUpUnitName = CleanUpUnit:GetName() - local CleanUpGroup = CleanUpUnit:GetGroup() - -- TODO Client bug in 1.5.3 - if CleanUpGroup:IsAlive() then - local CleanUpGroupUnits = CleanUpGroup:GetUnits() - if #CleanUpGroupUnits == 1 then - local CleanUpGroupName = CleanUpGroup:GetName() - CleanUpGroup:Destroy() - else - CleanUpUnit:Destroy() - end - self.CleanUpList[CleanUpUnitName] = nil - end - end -end - - - ---- Destroys a missile from the simulator, but checks first if it is still existing! --- @param #CLEANUP_AIRBASE self --- @param Dcs.DCSTypes#Weapon MissileObject -function CLEANUP_AIRBASE.__:DestroyMissile( MissileObject ) - self:F( { MissileObject } ) - - if MissileObject and MissileObject:isExist() then - MissileObject:destroy() - self:T( "MissileObject Destroyed") - end -end - ---- @param #CLEANUP_AIRBASE self --- @param Core.Event#EVENTDATA EventData -function CLEANUP_AIRBASE.__:OnEventBirth( EventData ) - self:F( { EventData } ) - - self.CleanUpList[EventData.IniDCSUnitName] = {} - self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnit = EventData.IniUnit - self.CleanUpList[EventData.IniDCSUnitName].CleanUpGroup = EventData.IniGroup - self.CleanUpList[EventData.IniDCSUnitName].CleanUpGroupName = EventData.IniDCSGroupName - self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnitName = EventData.IniDCSUnitName - -end - - ---- Detects if a crash event occurs. --- Crashed units go into a CleanUpList for removal. --- @param #CLEANUP_AIRBASE self --- @param Core.Event#EVENTDATA Event -function CLEANUP_AIRBASE.__:OnEventCrash( Event ) - self:F( { Event } ) - - --TODO: This stuff is not working due to a DCS bug. Burning units cannot be destroyed. - -- self:T("before getGroup") - -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired - -- self:T("after getGroup") - -- _grp:destroy() - -- self:T("after deactivateGroup") - -- event.initiator:destroy() - - if Event.IniDCSUnitName and Event.IniCategory == Object.Category.UNIT then - self.CleanUpList[Event.IniDCSUnitName] = {} - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniUnit - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniGroup - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName - end - -end - ---- Detects if a unit shoots a missile. --- If this occurs within one of the airbases, then the weapon used must be destroyed. --- @param #CLEANUP_AIRBASE self --- @param Core.Event#EVENTDATA Event -function CLEANUP_AIRBASE.__:OnEventShot( Event ) - self:F( { Event } ) - - -- Test if the missile was fired within one of the CLEANUP_AIRBASE.AirbaseNames. - if self:IsInAirbase( Event.IniUnit:GetVec2() ) then - -- Okay, the missile was fired within the CLEANUP_AIRBASE.AirbaseNames, destroy the fired weapon. - self:DestroyMissile( Event.Weapon ) - end -end - ---- Detects if the Unit has an S_EVENT_HIT within the given AirbaseNames. If this is the case, destroy the unit. --- @param #CLEANUP_AIRBASE self --- @param Core.Event#EVENTDATA Event -function CLEANUP_AIRBASE.__:OnEventHit( Event ) - self:F( { Event } ) - - if Event.IniUnit then - if self:IsInAirbase( Event.IniUnit:GetVec2() ) then - self:T( { "Life: ", Event.IniDCSUnitName, ' = ', Event.IniUnit:GetLife(), "/", Event.IniUnit:GetLife0() } ) - if Event.IniUnit:GetLife() < Event.IniUnit:GetLife0() then - self:T( "CleanUp: Destroy: " .. Event.IniDCSUnitName ) - CLEANUP_AIRBASE.__:DestroyUnit( Event.IniUnit ) - end - end - end - - if Event.TgtUnit then - if self:IsInAirbase( Event.TgtUnit:GetVec2() ) then - self:T( { "Life: ", Event.TgtDCSUnitName, ' = ', Event.TgtUnit:GetLife(), "/", Event.TgtUnit:GetLife0() } ) - if Event.TgtUnit:GetLife() < Event.TgtUnit:GetLife0() then - self:T( "CleanUp: Destroy: " .. Event.TgtDCSUnitName ) - CLEANUP_AIRBASE.__:DestroyUnit( Event.TgtUnit ) - end - end - end -end - ---- Add the @{DCSWrapper.Unit#Unit} to the CleanUpList for CleanUp. --- @param #CLEANUP_AIRBASE self --- @param Wrapper.Unit#UNIT CleanUpUnit --- @oaram #string CleanUpUnitName -function CLEANUP_AIRBASE.__:AddForCleanUp( CleanUpUnit, CleanUpUnitName ) - self:F( { CleanUpUnit, CleanUpUnitName } ) - - self.CleanUpList[CleanUpUnitName] = {} - self.CleanUpList[CleanUpUnitName].CleanUpUnit = CleanUpUnit - self.CleanUpList[CleanUpUnitName].CleanUpUnitName = CleanUpUnitName - - local CleanUpGroup = CleanUpUnit:GetGroup() - - self.CleanUpList[CleanUpUnitName].CleanUpGroup = CleanUpGroup - self.CleanUpList[CleanUpUnitName].CleanUpGroupName = CleanUpGroup:GetName() - self.CleanUpList[CleanUpUnitName].CleanUpTime = timer.getTime() - self.CleanUpList[CleanUpUnitName].CleanUpMoved = false - - self:T( { "CleanUp: Add to CleanUpList: ", CleanUpGroup:GetName(), CleanUpUnitName } ) - -end - ---- Detects if the Unit has an S_EVENT_ENGINE_SHUTDOWN or an S_EVENT_HIT within the given AirbaseNames. If this is the case, add the Group to the CLEANUP_AIRBASE List. --- @param #CLEANUP_AIRBASE.__ self --- @param Core.Event#EVENTDATA Event -function CLEANUP_AIRBASE.__:EventAddForCleanUp( Event ) - - self:F({Event}) - - - if Event.IniDCSUnit and Event.IniCategory == Object.Category.UNIT then - if self.CleanUpList[Event.IniDCSUnitName] == nil then - if self:IsInAirbase( Event.IniUnit:GetVec2() ) then - self:AddForCleanUp( Event.IniUnit, Event.IniDCSUnitName ) - end - end - end - - if Event.TgtDCSUnit and Event.TgtCategory == Object.Category.UNIT then - if self.CleanUpList[Event.TgtDCSUnitName] == nil then - if self:IsInAirbase( Event.TgtUnit:GetVec2() ) then - self:AddForCleanUp( Event.TgtUnit, Event.TgtDCSUnitName ) - end - end - end - -end - - ---- At the defined time interval, CleanUp the Groups within the CleanUpList. --- @param #CLEANUP_AIRBASE self -function CLEANUP_AIRBASE.__:CleanUpSchedule() - - local CleanUpCount = 0 - for CleanUpUnitName, CleanUpListData in pairs( self.CleanUpList ) do - CleanUpCount = CleanUpCount + 1 - - local CleanUpUnit = CleanUpListData.CleanUpUnit -- Wrapper.Unit#UNIT - local CleanUpGroupName = CleanUpListData.CleanUpGroupName - - if CleanUpUnit:IsAlive() ~= nil then - - if _DATABASE:GetStatusGroup( CleanUpGroupName ) ~= "ReSpawn" then - - local CleanUpCoordinate = CleanUpUnit:GetCoordinate() - - self:T( { "CleanUp Scheduler", CleanUpUnitName } ) - if CleanUpUnit:GetLife() <= CleanUpUnit:GetLife0() * 0.95 then - if CleanUpUnit:IsAboveRunway() then - if CleanUpUnit:InAir() then - - local CleanUpLandHeight = CleanUpCoordinate:GetLandHeight() - local CleanUpUnitHeight = CleanUpCoordinate.y - CleanUpLandHeight - - if CleanUpUnitHeight < 100 then - self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because below safe height and damaged." } ) - self:DestroyUnit( CleanUpUnit ) - end - else - self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because on runway and damaged." } ) - self:DestroyUnit( CleanUpUnit ) - end - end - end - -- Clean Units which are waiting for a very long time in the CleanUpZone. - if CleanUpUnit then - local CleanUpUnitVelocity = CleanUpUnit:GetVelocityKMH() - if CleanUpUnitVelocity < 1 then - if CleanUpListData.CleanUpMoved then - if CleanUpListData.CleanUpTime + 180 <= timer.getTime() then - self:T( { "CleanUp Scheduler", "Destroy due to not moving anymore " .. CleanUpUnitName } ) - self:DestroyUnit( CleanUpUnit ) - end - end - else - CleanUpListData.CleanUpTime = timer.getTime() - CleanUpListData.CleanUpMoved = true - end - end - - else - -- Do nothing ... - self.CleanUpList[CleanUpUnitName] = nil - end - else - self:T( "CleanUp: Group " .. CleanUpUnitName .. " cannot be found in DCS RTE, removing ..." ) - self.CleanUpList[CleanUpUnitName] = nil - end - end - self:T(CleanUpCount) - - return true -end - ---- **Functional** -- Spawn dynamically new GROUPs in your missions. --- --- ![Banner Image](..\Presentations\SPAWN\SPAWN.JPG) --- --- ==== --- --- The documentation of the SPAWN class can be found further in this document. --- --- ==== --- --- # Demo Missions --- --- ### [SPAWN Demo Missions source code](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/SPA%20-%20Spawning) --- --- ### [SPAWN Demo Missions, only for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SPA%20-%20Spawning) --- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) --- --- ==== --- --- # YouTube Channel --- --- ### [SPAWN YouTube Channel](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl1jirWIo4t4YxqN-HxjqRkL) --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **Aaron**: Posed the idea for Group position randomization at SpawnInZone and make the Unit randomization separate from the Group randomization. --- * [**Entropy**](https://forums.eagle.ru/member.php?u=111471), **Afinegan**: Came up with the requirement for AIOnOff(). --- --- ### Authors: --- --- * **FlightControl**: Design & Programming --- --- @module Spawn - -----BASE:TraceClass("SPAWN") - - ---- SPAWN Class --- @type SPAWN --- @field ClassName --- @field #string SpawnTemplatePrefix --- @field #string SpawnAliasPrefix --- @field #number AliveUnits --- @field #number MaxAliveUnits --- @field #number SpawnIndex --- @field #number MaxAliveGroups --- @field #SPAWN.SpawnZoneTable SpawnZoneTable --- @extends Core.Base#BASE - - ---- # SPAWN class, extends @{Base#BASE} --- --- The SPAWN class allows to spawn dynamically new groups. --- Each SPAWN object needs to be have a related **template group** setup in the Mission Editor (ME), --- which is a normal group with the **Late Activation** flag set. --- This template group will never be activated in your mission. --- SPAWN uses that **template group** to reference to all the characteristics --- (air, ground, livery, unit composition, formation, skill level etc) of each new group to be spawned. --- --- Therefore, when creating a SPAWN object, the @{#SPAWN.New} and @{#SPAWN.NewWithAlias} require --- **the name of the template group** to be given as a string to those constructor methods. --- --- Initialization settings can be applied on the SPAWN object, --- which modify the behaviour or the way groups are spawned. --- These initialization methods have the prefix **Init**. --- There are also spawn methods with the prefix **Spawn** and will spawn new groups in various ways. --- --- ### IMPORTANT! The methods with prefix **Init** must be used before any methods with prefix **Spawn** method are used, or unexpected results may appear!!! --- --- Because SPAWN can spawn multiple groups of a template group, --- SPAWN has an **internal index** that keeps track --- which was the latest group that was spawned. --- --- **Limits** can be set on how many groups can be spawn in each SPAWN object, --- using the method @{#SPAWN.InitLimit}. SPAWN has 2 kind of limits: --- --- * The maximum amount of @{Unit}s that can be **alive** at the same time... --- * The maximum amount of @{Group}s that can be **spawned**... This is more of a **resource**-type of limit. --- --- When new groups get spawned using the **Spawn** methods, --- it will be evaluated whether any limits have been reached. --- When no spawn limit is reached, a new group will be created by the spawning methods, --- and the internal index will be increased with 1. --- --- These limits ensure that your mission does not accidentally get flooded with spawned groups. --- Additionally, it also guarantees that independent of the group composition, --- at any time, the most optimal amount of groups are alive in your mission. --- For example, if your template group has a group composition of 10 units, and you specify a limit of 100 units alive at the same time, --- with unlimited resources = :InitLimit( 100, 0 ) and 10 groups are alive, but two groups have only one unit alive in the group, --- then a sequent Spawn(Scheduled) will allow a new group to be spawned!!! --- --- ### IMPORTANT!! If a limit has been reached, it is possible that a **Spawn** method returns **nil**, meaning, no @{Group} had been spawned!!! --- --- Spawned groups get **the same name** as the name of the template group. --- Spawned units in those groups keep _by default_ **the same name** as the name of the template group. --- However, because multiple groups and units are created from the template group, --- a suffix is added to each spawned group and unit. --- --- Newly spawned groups will get the following naming structure at run-time: --- --- 1. Spawned groups will have the name _GroupName_#_nnn_, where _GroupName_ is the name of the **template group**, --- and _nnn_ is a **counter from 0 to 999**. --- 2. Spawned units will have the name _GroupName_#_nnn_-_uu_, --- where _uu_ is a **counter from 0 to 99** for each new spawned unit belonging to the group. --- --- That being said, there is a way to keep the same unit names! --- The method @{#SPAWN.InitKeepUnitNames}() will keep the same unit names as defined within the template group, thus: --- --- 3. Spawned units will have the name _UnitName_#_nnn_-_uu_, --- where _UnitName_ is the **unit name as defined in the template group*, --- and _uu_ is a **counter from 0 to 99** for each new spawned unit belonging to the group. --- --- Some **additional notes that need to be considered!!**: --- --- * templates are actually groups defined within the mission editor, with the flag "Late Activation" set. --- As such, these groups are never used within the mission, but are used by the @{#SPAWN} module. --- * It is important to defined BEFORE you spawn new groups, --- a proper initialization of the SPAWN instance is done with the options you want to use. --- * When designing a mission, NEVER name groups using a "#" within the name of the group Spawn template(s), --- or the SPAWN module logic won't work anymore. --- --- ## SPAWN construction methods --- --- Create a new SPAWN object with the @{#SPAWN.New}() or the @{#SPAWN.NewWithAlias}() methods: --- --- * @{#SPAWN.New}(): Creates a new SPAWN object taking the name of the group that represents the GROUP template (definition). --- * @{#SPAWN.NewWithAlias}(): Creates a new SPAWN object taking the name of the group that represents the GROUP template (definition), and gives each spawned @{Group} an different name. --- --- It is important to understand how the SPAWN class works internally. The SPAWN object created will contain internally a list of groups that will be spawned and that are already spawned. --- The initialization methods will modify this list of groups so that when a group gets spawned, ALL information is already prepared when spawning. This is done for performance reasons. --- So in principle, the group list will contain all parameters and configurations after initialization, and when groups get actually spawned, this spawning can be done quickly and efficient. --- --- ## SPAWN **Init**ialization methods --- --- A spawn object will behave differently based on the usage of **initialization** methods, which all start with the **Init** prefix: --- --- ### Unit Names --- --- * @{#SPAWN.InitKeepUnitNames}(): Keeps the unit names as defined within the mission editor, but note that anything after a # mark is ignored, and any spaces before and after the resulting name are removed. IMPORTANT! This method MUST be the first used after :New !!! --- --- ### Route randomization --- --- * @{#SPAWN.InitRandomizeRoute}(): Randomize the routes of spawned groups, and for air groups also optionally the height. --- --- ### Group composition randomization --- --- * @{#SPAWN.InitRandomizeTemplate}(): Randomize the group templates so that when a new group is spawned, a random group template is selected from one of the templates defined. --- --- ### Uncontrolled --- --- * @{#SPAWN.InitUnControlled}(): Spawn plane groups uncontrolled. --- --- ### Array formation --- --- * @{#SPAWN.InitArray}(): Make groups visible before they are actually activated, and order these groups like a batallion in an array. --- --- ### Position randomization --- --- * @{#SPAWN.InitRandomizePosition}(): Randomizes the position of @{Group}s that are spawned within a **radius band**, given an Outer and Inner radius, from the point that the spawn happens. --- * @{#SPAWN.InitRandomizeUnits}(): Randomizes the @{Unit}s in the @{Group} that is spawned within a **radius band**, given an Outer and Inner radius. --- * @{#SPAWN.InitRandomizeZones}(): Randomizes the spawning between a predefined list of @{Zone}s that are declared using this function. Each zone can be given a probability factor. --- --- ### Enable / Disable AI when spawning a new @{Group} --- --- * @{#SPAWN.InitAIOn}(): Turns the AI On when spawning the new @{Group} object. --- * @{#SPAWN.InitAIOff}(): Turns the AI Off when spawning the new @{Group} object. --- * @{#SPAWN.InitAIOnOff}(): Turns the AI On or Off when spawning the new @{Group} object. --- --- ### Limit scheduled spawning --- --- * @{#SPAWN.InitLimit}(): Limits the amount of groups that can be alive at the same time and that can be dynamically spawned. --- --- ### Delay initial scheduled spawn --- --- * @{#SPAWN.InitDelayOnOff}(): Turns the inital delay On/Off when scheduled spawning the first @{Group} object. --- * @{#SPAWN.InitDelayOn}(): Turns the inital delay On when scheduled spawning the first @{Group} object. --- * @{#SPAWN.InitDelayOff}(): Turns the inital delay Off when scheduled spawning the first @{Group} object. --- --- ### Repeat spawned @{Group}s upon landing --- --- * @{#SPAWN.InitRepeat}() or @{#SPAWN.InitRepeatOnLanding}(): This method is used to re-spawn automatically the same group after it has landed. --- * @{#SPAWN.InitRepeatOnEngineShutDown}(): This method is used to re-spawn automatically the same group after it has landed and it shuts down the engines at the ramp. --- --- --- ## SPAWN **Spawn** methods --- --- Groups can be spawned at different times and methods: --- --- ### **Single** spawning methods --- --- * @{#SPAWN.Spawn}(): Spawn one new group based on the last spawned index. --- * @{#SPAWN.ReSpawn}(): Re-spawn a group based on a given index. --- * @{#SPAWN.SpawnFromVec3}(): Spawn a new group from a Vec3 coordinate. (The group will can be spawned at a point in the air). --- * @{#SPAWN.SpawnFromVec2}(): Spawn a new group from a Vec2 coordinate. (The group will be spawned at land height ). --- * @{#SPAWN.SpawnFromStatic}(): Spawn a new group from a structure, taking the position of a @{Static}. --- * @{#SPAWN.SpawnFromUnit}(): Spawn a new group taking the position of a @{Unit}. --- * @{#SPAWN.SpawnInZone}(): Spawn a new group in a @{Zone}. --- * @{#SPAWN.SpawnAtAirbase}(): Spawn a new group at an @{Airbase}, which can be an airdrome, ship or helipad. --- --- Note that @{#SPAWN.Spawn} and @{#SPAWN.ReSpawn} return a @{GROUP#GROUP.New} object, that contains a reference to the DCSGroup object. --- You can use the @{GROUP} object to do further actions with the DCSGroup. --- --- ### **Scheduled** spawning methods --- --- * @{#SPAWN.SpawnScheduled}(): Spawn groups at scheduled but randomized intervals. --- * @{#SPAWN.SpawnScheduledStart}(): Start or continue to spawn groups at scheduled time intervals. --- * @{#SPAWN.SpawnScheduledStop}(): Stop the spawning of groups at scheduled time intervals. --- --- --- --- ## Retrieve alive GROUPs spawned by the SPAWN object --- --- The SPAWN class administers which GROUPS it has reserved (in stock) or has created during mission execution. --- Every time a SPAWN object spawns a new GROUP object, a reference to the GROUP object is added to an internal table of GROUPS. --- SPAWN provides methods to iterate through that internal GROUP object reference table: --- --- * @{#SPAWN.GetFirstAliveGroup}(): Will find the first alive GROUP it has spawned, and return the alive GROUP object and the first Index where the first alive GROUP object has been found. --- * @{#SPAWN.GetNextAliveGroup}(): Will find the next alive GROUP object from a given Index, and return a reference to the alive GROUP object and the next Index where the alive GROUP has been found. --- * @{#SPAWN.GetLastAliveGroup}(): Will find the last alive GROUP object, and will return a reference to the last live GROUP object and the last Index where the last alive GROUP object has been found. --- --- You can use the methods @{#SPAWN.GetFirstAliveGroup}() and sequently @{#SPAWN.GetNextAliveGroup}() to iterate through the alive GROUPS within the SPAWN object, and to actions... See the respective methods for an example. --- The method @{#SPAWN.GetGroupFromIndex}() will return the GROUP object reference from the given Index, dead or alive... --- --- ## Spawned cleaning of inactive groups --- --- Sometimes, it will occur during a mission run-time, that ground or especially air objects get damaged, and will while being damged stop their activities, while remaining alive. --- In such cases, the SPAWN object will just sit there and wait until that group gets destroyed, but most of the time it won't, --- and it may occur that no new groups are or can be spawned as limits are reached. --- To prevent this, a @{#SPAWN.InitCleanUp}() initialization method has been defined that will silently monitor the status of each spawned group. --- Once a group has a velocity = 0, and has been waiting for a defined interval, that group will be cleaned or removed from run-time. --- There is a catch however :-) If a damaged group has returned to an airbase within the coalition, that group will not be considered as "lost"... --- In such a case, when the inactive group is cleaned, a new group will Re-spawned automatically. --- This models AI that has succesfully returned to their airbase, to restart their combat activities. --- Check the @{#SPAWN.InitCleanUp}() for further info. --- --- ## Catch the @{Group} Spawn Event in a callback function! --- --- When using the @{#SPAWN.SpawnScheduled)() method, new @{Group}s are created following the spawn time interval parameters. --- When a new @{Group} is spawned, you maybe want to execute actions with that group spawned at the spawn event. --- The SPAWN class supports this functionality through the method @{#SPAWN.OnSpawnGroup}( **function( SpawnedGroup ) end ** ), --- which takes a function as a parameter that you can define locally. --- Whenever a new @{Group} is spawned, the given function is called, and the @{Group} that was just spawned, is given as a parameter. --- As a result, your spawn event handling function requires one parameter to be declared, which will contain the spawned @{Group} object. --- A coding example is provided at the description of the @{#SPAWN.OnSpawnGroup}( **function( SpawnedGroup ) end ** ) method. --- --- ## Delay the initial spawning --- --- When using the @{#SPAWN.SpawnScheduled)() method, the default behaviour of this method will be that it will spawn the initial (first) @{Group} --- immediately when :SpawnScheduled() is initiated. The methods @{#SPAWN.InitDelayOnOff}() and @{#SPAWN.InitDelayOn}() can be used to --- activate a delay before the first @{Group} is spawned. For completeness, a method @{#SPAWN.InitDelayOff}() is also available, that --- can be used to switch off the initial delay. Because there is no delay by default, this method would only be used when a --- @{#SPAWN.SpawnScheduledStop}() ; @{#SPAWN.SpawnScheduledStart}() sequence would have been used. --- --- --- @field #SPAWN SPAWN --- -SPAWN = { - ClassName = "SPAWN", - SpawnTemplatePrefix = nil, - SpawnAliasPrefix = nil, -} - - ---- Enumerator for spawns at airbases --- @type SPAWN.Takeoff --- @extends Wrapper.Group#GROUP.Takeoff - ---- @field #SPAWN.Takeoff Takeoff -SPAWN.Takeoff = GROUP.Takeoff - - ---- @type SPAWN.SpawnZoneTable --- @list SpawnZone - - ---- Creates the main object to spawn a @{Group} defined in the DCS ME. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template. Each new group will have the name starting with SpawnTemplatePrefix. --- @return #SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ) --- @usage local Plane = SPAWN:New( "Plane" ) -- Creates a new local variable that can initiate new planes with the name "Plane#ddd" using the template "Plane" as defined within the ME. -function SPAWN:New( SpawnTemplatePrefix ) - local self = BASE:Inherit( self, BASE:New() ) -- #SPAWN - self:F( { SpawnTemplatePrefix } ) - - local TemplateGroup = Group.getByName( SpawnTemplatePrefix ) - if TemplateGroup then - self.SpawnTemplatePrefix = SpawnTemplatePrefix - self.SpawnIndex = 0 - self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. - self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. - self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! - self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. - self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. - self.SpawnInitLimit = false -- By default, no InitLimit - self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. - self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. - self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. - self.AIOnOff = true -- The AI is on by default when spawning a group. - self.SpawnUnControlled = false - self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. - self.DelayOnOff = false -- No intial delay when spawning the first group. - self.Grouping = nil -- No grouping - - self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. - else - error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) - end - - self:SetEventPriority( 5 ) - - return self -end - ---- Creates a new SPAWN instance to create new groups based on the defined template and using a new alias for each new group. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template. --- @param #string SpawnAliasPrefix is the name that will be given to the Group at runtime. --- @return #SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- Spawn_BE_KA50 = SPAWN:NewWithAlias( 'BE KA-50@RAMP-Ground Defense', 'Helicopter Attacking a City' ) --- @usage local PlaneWithAlias = SPAWN:NewWithAlias( "Plane", "Bomber" ) -- Creates a new local variable that can instantiate new planes with the name "Bomber#ddd" using the template "Plane" as defined within the ME. -function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { SpawnTemplatePrefix, SpawnAliasPrefix } ) - - local TemplateGroup = Group.getByName( SpawnTemplatePrefix ) - if TemplateGroup then - self.SpawnTemplatePrefix = SpawnTemplatePrefix - self.SpawnAliasPrefix = SpawnAliasPrefix - self.SpawnIndex = 0 - self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. - self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. - self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! - self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. - self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. - self.SpawnInitLimit = false -- By default, no InitLimit - self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. - self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. - self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. - self.AIOnOff = true -- The AI is on by default when spawning a group. - self.SpawnUnControlled = false - self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. - self.DelayOnOff = false -- No intial delay when spawning the first group. - self.Grouping = nil - - self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. - else - error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) - end - - self:SetEventPriority( 5 ) - - return self -end - - ---- Limits the Maximum amount of Units that can be alive at the same time, and the maximum amount of groups that can be spawned. --- Note that this method is exceptionally important to balance the performance of the mission. Depending on the machine etc, a mission can only process a maximum amount of units. --- If the time interval must be short, but there should not be more Units or Groups alive than a maximum amount of units, then this method should be used... --- When a @{#SPAWN.New} is executed and the limit of the amount of units alive is reached, then no new spawn will happen of the group, until some of these units of the spawn object will be destroyed. --- @param #SPAWN self --- @param #number SpawnMaxUnitsAlive The maximum amount of units that can be alive at runtime. --- @param #number SpawnMaxGroups The maximum amount of groups that can be spawned. When the limit is reached, then no more actual spawns will happen of the group. --- This parameter is useful to define a maximum amount of airplanes, ground troops, helicopters, ships etc within a supply area. --- This parameter accepts the value 0, which defines that there are no maximum group limits, but there are limits on the maximum of units that can be alive at the same time. --- @return #SPAWN self --- @usage --- -- NATO helicopters engaging in the battle field. --- -- This helicopter group consists of one Unit. So, this group will SPAWN maximum 2 groups simultaneously within the DCSRTE. --- -- There will be maximum 24 groups spawned during the whole mission lifetime. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitLimit( 2, 24 ) -function SPAWN:InitLimit( SpawnMaxUnitsAlive, SpawnMaxGroups ) - self:F( { self.SpawnTemplatePrefix, SpawnMaxUnitsAlive, SpawnMaxGroups } ) - - self.SpawnInitLimit = true - self.SpawnMaxUnitsAlive = SpawnMaxUnitsAlive -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = SpawnMaxGroups -- The maximum amount of groups that can be spawned. - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:_InitializeSpawnGroups( SpawnGroupID ) - end - - return self -end - ---- Keeps the unit names as defined within the mission editor, --- but note that anything after a # mark is ignored, --- and any spaces before and after the resulting name are removed. --- IMPORTANT! This method MUST be the first used after :New !!! --- @param #SPAWN self --- @return #SPAWN self -function SPAWN:InitKeepUnitNames() - self:F( ) - - self.SpawnInitKeepUnitNames = true - - return self -end - - ---- Randomizes the defined route of the SpawnTemplatePrefix group in the ME. This is very useful to define extra variation of the behaviour of groups. --- @param #SPAWN self --- @param #number SpawnStartPoint is the waypoint where the randomization begins. --- Note that the StartPoint = 0 equaling the point where the group is spawned. --- @param #number SpawnEndPoint is the waypoint where the randomization ends counting backwards. --- This parameter is useful to avoid randomization to end at a waypoint earlier than the last waypoint on the route. --- @param #number SpawnRadius is the radius in meters in which the randomization of the new waypoints, with the original waypoint of the original template located in the middle ... --- @param #number SpawnHeight (optional) Specifies the **additional** height in meters that can be added to the base height specified at each waypoint in the ME. --- @return #SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). --- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. --- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitRandomizeRoute( 2, 2, 2000 ) -function SPAWN:InitRandomizeRoute( SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight ) - self:F( { self.SpawnTemplatePrefix, SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight } ) - - self.SpawnRandomizeRoute = true - self.SpawnRandomizeRouteStartPoint = SpawnStartPoint - self.SpawnRandomizeRouteEndPoint = SpawnEndPoint - self.SpawnRandomizeRouteRadius = SpawnRadius - self.SpawnRandomizeRouteHeight = SpawnHeight - - for GroupID = 1, self.SpawnMaxGroups do - self:_RandomizeRoute( GroupID ) - end - - return self -end - ---- Randomizes the position of @{Group}s that are spawned within a **radius band**, given an Outer and Inner radius, from the point that the spawn happens. --- @param #SPAWN self --- @param #boolean RandomizePosition If true, SPAWN will perform the randomization of the @{Group}s position between a given outer and inner radius. --- @param Dcs.DCSTypes#Distance OuterRadius (optional) The outer radius in meters where the new group will be spawned. --- @param Dcs.DCSTypes#Distance InnerRadius (optional) The inner radius in meters where the new group will NOT be spawned. --- @return #SPAWN -function SPAWN:InitRandomizePosition( RandomizePosition, OuterRadius, InnerRadius ) - self:F( { self.SpawnTemplatePrefix, RandomizePosition, OuterRadius, InnerRadius } ) - - self.SpawnRandomizePosition = RandomizePosition or false - self.SpawnRandomizePositionOuterRadius = OuterRadius or 0 - self.SpawnRandomizePositionInnerRadius = InnerRadius or 0 - - for GroupID = 1, self.SpawnMaxGroups do - self:_RandomizeRoute( GroupID ) - end - - return self -end - - ---- Randomizes the UNITs that are spawned within a radius band given an Outer and Inner radius. --- @param #SPAWN self --- @param #boolean RandomizeUnits If true, SPAWN will perform the randomization of the @{UNIT}s position within the group between a given outer and inner radius. --- @param Dcs.DCSTypes#Distance OuterRadius (optional) The outer radius in meters where the new group will be spawned. --- @param Dcs.DCSTypes#Distance InnerRadius (optional) The inner radius in meters where the new group will NOT be spawned. --- @return #SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). --- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. --- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitRandomizeRoute( 2, 2, 2000 ) -function SPAWN:InitRandomizeUnits( RandomizeUnits, OuterRadius, InnerRadius ) - self:F( { self.SpawnTemplatePrefix, RandomizeUnits, OuterRadius, InnerRadius } ) - - self.SpawnRandomizeUnits = RandomizeUnits or false - self.SpawnOuterRadius = OuterRadius or 0 - self.SpawnInnerRadius = InnerRadius or 0 - - for GroupID = 1, self.SpawnMaxGroups do - self:_RandomizeRoute( GroupID ) - end - - return self -end - ---- This method is rather complicated to understand. But I'll try to explain. --- This method becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, --- but they will all follow the same Template route and have the same prefix name. --- In other words, this method randomizes between a defined set of groups the template to be used for each new spawn of a group. --- @param #SPAWN self --- @param #string SpawnTemplatePrefixTable A table with the names of the groups defined within the mission editor, from which one will be choosen when a new group will be spawned. --- @return #SPAWN --- @usage --- -- NATO Tank Platoons invading Gori. --- -- Choose between 13 different 'US Tank Platoon' configurations for each new SPAWN the Group to be spawned for the --- -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SpawnTemplatePrefixes. --- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and --- -- with a limit set of maximum 12 Units alive simulteneously and 150 Groups to be spawned during the whole mission. --- Spawn_US_Platoon = { 'US Tank Platoon 1', 'US Tank Platoon 2', 'US Tank Platoon 3', 'US Tank Platoon 4', 'US Tank Platoon 5', --- 'US Tank Platoon 6', 'US Tank Platoon 7', 'US Tank Platoon 8', 'US Tank Platoon 9', 'US Tank Platoon 10', --- 'US Tank Platoon 11', 'US Tank Platoon 12', 'US Tank Platoon 13' } --- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) -function SPAWN:InitRandomizeTemplate( SpawnTemplatePrefixTable ) - self:F( { self.SpawnTemplatePrefix, SpawnTemplatePrefixTable } ) - - self.SpawnTemplatePrefixTable = SpawnTemplatePrefixTable - self.SpawnRandomizeTemplate = true - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:_RandomizeTemplate( SpawnGroupID ) - end - - return self -end - ---- When spawning a new group, make the grouping of the units according the InitGrouping setting. --- @param #SPAWN self --- @param #number Grouping Indicates the maximum amount of units in the group. --- @return #SPAWN -function SPAWN:InitGrouping( Grouping ) -- R2.2 - self:F( { self.SpawnTemplatePrefix, Grouping } ) - - self.SpawnGrouping = Grouping - - return self -end - - - ---TODO: Add example. ---- This method provides the functionality to randomize the spawning of the Groups at a given list of zones of different types. --- @param #SPAWN self --- @param #table SpawnZoneTable A table with @{Zone} objects. If this table is given, then each spawn will be executed within the given list of @{Zone}s objects. --- @return #SPAWN --- @usage --- -- NATO Tank Platoons invading Gori. --- -- Choose between 3 different zones for each new SPAWN the Group to be executed, regardless of the zone type. -function SPAWN:InitRandomizeZones( SpawnZoneTable ) - self:F( { self.SpawnTemplatePrefix, SpawnZoneTable } ) - - self.SpawnZoneTable = SpawnZoneTable - self.SpawnRandomizeZones = true - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:_RandomizeZones( SpawnGroupID ) - end - - return self -end - - - - - ---- For planes and helicopters, when these groups go home and land on their home airbases and farps, they normally would taxi to the parking spot, shut-down their engines and wait forever until the Group is removed by the runtime environment. --- This method is used to re-spawn automatically (so no extra call is needed anymore) the same group after it has landed. --- This will enable a spawned group to be re-spawned after it lands, until it is destroyed... --- Note: When the group is respawned, it will re-spawn from the original airbase where it took off. --- So ensure that the routes for groups that respawn, always return to the original airbase, or players may get confused ... --- @param #SPAWN self --- @return #SPAWN self --- @usage --- -- RU Su-34 - AI Ship Attack --- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. --- SpawnRU_SU34 = SPAWN:New( 'TF1 RU Su-34 Krymsk@AI - Attack Ships' ):Schedule( 2, 3, 1800, 0.4 ):SpawnUncontrolled():InitRandomizeRoute( 1, 1, 3000 ):RepeatOnEngineShutDown() -function SPAWN:InitRepeat() - self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } ) - - self.Repeat = true - self.RepeatOnEngineShutDown = false - self.RepeatOnLanding = true - - return self -end - ---- Respawn group after landing. --- @param #SPAWN self --- @return #SPAWN self -function SPAWN:InitRepeatOnLanding() - self:F( { self.SpawnTemplatePrefix } ) - - self:InitRepeat() - self.RepeatOnEngineShutDown = false - self.RepeatOnLanding = true - - return self -end - - ---- Respawn after landing when its engines have shut down. --- @param #SPAWN self --- @return #SPAWN self -function SPAWN:InitRepeatOnEngineShutDown() - self:F( { self.SpawnTemplatePrefix } ) - - self:InitRepeat() - self.RepeatOnEngineShutDown = true - self.RepeatOnLanding = false - - return self -end - - ---- CleanUp groups when they are still alive, but inactive. --- When groups are still alive and have become inactive due to damage and are unable to contribute anything, then this group will be removed at defined intervals in seconds. --- @param #SPAWN self --- @param #string SpawnCleanUpInterval The interval to check for inactive groups within seconds. --- @return #SPAWN self --- @usage Spawn_Helicopter:CleanUp( 20 ) -- CleanUp the spawning of the helicopters every 20 seconds when they become inactive. -function SPAWN:InitCleanUp( SpawnCleanUpInterval ) - self:F( { self.SpawnTemplatePrefix, SpawnCleanUpInterval } ) - - self.SpawnCleanUpInterval = SpawnCleanUpInterval - self.SpawnCleanUpTimeStamps = {} - - local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup() - self:T( { "CleanUp Scheduler:", SpawnGroup } ) - - --self.CleanUpFunction = routines.scheduleFunction( self._SpawnCleanUpScheduler, { self }, timer.getTime() + 1, SpawnCleanUpInterval ) - self.CleanUpScheduler = SCHEDULER:New( self, self._SpawnCleanUpScheduler, {}, 1, SpawnCleanUpInterval, 0.2 ) - return self -end - - - ---- Makes the groups visible before start (like a batallion). --- The method will take the position of the group as the first position in the array. --- @param #SPAWN self --- @param #number SpawnAngle The angle in degrees how the groups and each unit of the group will be positioned. --- @param #number SpawnWidth The amount of Groups that will be positioned on the X axis. --- @param #number SpawnDeltaX The space between each Group on the X-axis. --- @param #number SpawnDeltaY The space between each Group on the Y-axis. --- @return #SPAWN self --- @usage --- -- Define an array of Groups. --- Spawn_BE_Ground = SPAWN:New( 'BE Ground' ):InitLimit( 2, 24 ):InitArray( 90, "Diamond", 10, 100, 50 ) -function SPAWN:InitArray( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) - self:F( { self.SpawnTemplatePrefix, SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY } ) - - self.SpawnVisible = true -- When the first Spawn executes, all the Groups need to be made visible before start. - - local SpawnX = 0 - local SpawnY = 0 - local SpawnXIndex = 0 - local SpawnYIndex = 0 - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:T( { SpawnX, SpawnY, SpawnXIndex, SpawnYIndex } ) - - self.SpawnGroups[SpawnGroupID].Visible = true - self.SpawnGroups[SpawnGroupID].Spawned = false - - SpawnXIndex = SpawnXIndex + 1 - if SpawnWidth and SpawnWidth ~= 0 then - if SpawnXIndex >= SpawnWidth then - SpawnXIndex = 0 - SpawnYIndex = SpawnYIndex + 1 - end - end - - local SpawnRootX = self.SpawnGroups[SpawnGroupID].SpawnTemplate.x - local SpawnRootY = self.SpawnGroups[SpawnGroupID].SpawnTemplate.y - - self:_TranslateRotate( SpawnGroupID, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) - - self.SpawnGroups[SpawnGroupID].SpawnTemplate.lateActivation = true - self.SpawnGroups[SpawnGroupID].SpawnTemplate.visible = true - - self.SpawnGroups[SpawnGroupID].Visible = true - - self:HandleEvent( EVENTS.Birth, self._OnBirth ) - self:HandleEvent( EVENTS.Dead, self._OnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._OnDeadOrCrash ) - if self.Repeat then - self:HandleEvent( EVENTS.Takeoff, self._OnTakeOff ) - self:HandleEvent( EVENTS.Land, self._OnLand ) - end - if self.RepeatOnEngineShutDown then - self:HandleEvent( EVENTS.EngineShutdown, self._OnEngineShutDown ) - end - - self.SpawnGroups[SpawnGroupID].Group = _DATABASE:Spawn( self.SpawnGroups[SpawnGroupID].SpawnTemplate ) - - SpawnX = SpawnXIndex * SpawnDeltaX - SpawnY = SpawnYIndex * SpawnDeltaY - end - - return self -end - -do -- AI methods - --- Turns the AI On or Off for the @{Group} when spawning. - -- @param #SPAWN self - -- @param #boolean AIOnOff A value of true sets the AI On, a value of false sets the AI Off. - -- @return #SPAWN The SPAWN object - function SPAWN:InitAIOnOff( AIOnOff ) - - self.AIOnOff = AIOnOff - return self - end - - --- Turns the AI On for the @{Group} when spawning. - -- @param #SPAWN self - -- @return #SPAWN The SPAWN object - function SPAWN:InitAIOn() - - return self:InitAIOnOff( true ) - end - - --- Turns the AI Off for the @{Group} when spawning. - -- @param #SPAWN self - -- @return #SPAWN The SPAWN object - function SPAWN:InitAIOff() - - return self:InitAIOnOff( false ) - end - -end -- AI methods - -do -- Delay methods - --- Turns the Delay On or Off for the first @{Group} scheduled spawning. - -- The default value is that for scheduled spawning, there is an initial delay when spawning the first @{Group}. - -- @param #SPAWN self - -- @param #boolean DelayOnOff A value of true sets the Delay On, a value of false sets the Delay Off. - -- @return #SPAWN The SPAWN object - function SPAWN:InitDelayOnOff( DelayOnOff ) - - self.DelayOnOff = DelayOnOff - return self - end - - --- Turns the Delay On for the @{Group} when spawning. - -- @param #SPAWN self - -- @return #SPAWN The SPAWN object - function SPAWN:InitDelayOn() - - return self:InitDelayOnOff( true ) - end - - --- Turns the Delay Off for the @{Group} when spawning. - -- @param #SPAWN self - -- @return #SPAWN The SPAWN object - function SPAWN:InitDelayOff() - - return self:InitDelayOnOff( false ) - end - -end -- Delay methods - ---- Will spawn a group based on the internal index. --- Note: Uses @{DATABASE} module defined in MOOSE. --- @param #SPAWN self --- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:Spawn() - self:F( { self.SpawnTemplatePrefix, self.SpawnIndex, self.AliveUnits } ) - - return self:SpawnWithIndex( self.SpawnIndex + 1 ) -end - ---- Will re-spawn a group based on a given index. --- Note: Uses @{DATABASE} module defined in MOOSE. --- @param #SPAWN self --- @param #string SpawnIndex The index of the group to be spawned. --- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:ReSpawn( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) - - if not SpawnIndex then - SpawnIndex = 1 - end - --- TODO: This logic makes DCS crash and i don't know why (yet). - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - local WayPoints = SpawnGroup and SpawnGroup.WayPoints or nil - if SpawnGroup then - local SpawnDCSGroup = SpawnGroup:GetDCSObject() - if SpawnDCSGroup then - SpawnGroup:Destroy() - end - end - - local SpawnGroup = self:SpawnWithIndex( SpawnIndex ) - if SpawnGroup and WayPoints then - -- If there were WayPoints set, then Re-Execute those WayPoints! - SpawnGroup:WayPointInitialize( WayPoints ) - SpawnGroup:WayPointExecute( 1, 5 ) - end - - if SpawnGroup.ReSpawnFunction then - SpawnGroup:ReSpawnFunction() - end - - SpawnGroup:ResetEvents() - - return SpawnGroup -end - ---- Will spawn a group with a specified index number. --- Uses @{DATABASE} global object defined in MOOSE. --- @param #SPAWN self --- @param #string SpawnIndex The index of the group to be spawned. --- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:SpawnWithIndex( SpawnIndex ) - self:F2( { SpawnTemplatePrefix = self.SpawnTemplatePrefix, SpawnIndex = SpawnIndex, AliveUnits = self.AliveUnits, SpawnMaxGroups = self.SpawnMaxGroups } ) - - if self:_GetSpawnIndex( SpawnIndex ) then - - if self.SpawnGroups[self.SpawnIndex].Visible then - self.SpawnGroups[self.SpawnIndex].Group:Activate() - else - - local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate - self:T( SpawnTemplate.name ) - - if SpawnTemplate then - - local PointVec3 = POINT_VEC3:New( SpawnTemplate.route.points[1].x, SpawnTemplate.route.points[1].alt, SpawnTemplate.route.points[1].y ) - self:T( { "Current point of ", self.SpawnTemplatePrefix, PointVec3 } ) - - -- If RandomizePosition, then Randomize the formation in the zone band, keeping the template. - if self.SpawnRandomizePosition then - local RandomVec2 = PointVec3:GetRandomVec2InRadius( self.SpawnRandomizePositionOuterRadius, self.SpawnRandomizePositionInnerRadius ) - local CurrentX = SpawnTemplate.units[1].x - local CurrentY = SpawnTemplate.units[1].y - SpawnTemplate.x = RandomVec2.x - SpawnTemplate.y = RandomVec2.y - for UnitID = 1, #SpawnTemplate.units do - SpawnTemplate.units[UnitID].x = SpawnTemplate.units[UnitID].x + ( RandomVec2.x - CurrentX ) - SpawnTemplate.units[UnitID].y = SpawnTemplate.units[UnitID].y + ( RandomVec2.y - CurrentY ) - self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - end - end - - -- If RandomizeUnits, then Randomize the formation at the start point. - if self.SpawnRandomizeUnits then - for UnitID = 1, #SpawnTemplate.units do - local RandomVec2 = PointVec3:GetRandomVec2InRadius( self.SpawnOuterRadius, self.SpawnInnerRadius ) - SpawnTemplate.units[UnitID].x = RandomVec2.x - SpawnTemplate.units[UnitID].y = RandomVec2.y - self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - end - end - - if SpawnTemplate.CategoryID == Group.Category.HELICOPTER or SpawnTemplate.CategoryID == Group.Category.AIRPLANE then - if SpawnTemplate.route.points[1].type == "TakeOffParking" then - SpawnTemplate.uncontrolled = self.SpawnUnControlled - end - end - end - - self:HandleEvent( EVENTS.Birth, self._OnBirth ) - self:HandleEvent( EVENTS.Dead, self._OnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._OnDeadOrCrash ) - if self.Repeat then - self:HandleEvent( EVENTS.Takeoff, self._OnTakeOff ) - self:HandleEvent( EVENTS.Land, self._OnLand ) - end - if self.RepeatOnEngineShutDown then - self:HandleEvent( EVENTS.EngineShutdown, self._OnEngineShutDown ) - end - - self.SpawnGroups[self.SpawnIndex].Group = _DATABASE:Spawn( SpawnTemplate ) - - local SpawnGroup = self.SpawnGroups[self.SpawnIndex].Group -- Wrapper.Group#GROUP - - --TODO: Need to check if this function doesn't need to be scheduled, as the group may not be immediately there! - if SpawnGroup then - - SpawnGroup:SetAIOnOff( self.AIOnOff ) - end - - self:T3( SpawnTemplate.name ) - - -- If there is a SpawnFunction hook defined, call it. - if self.SpawnFunctionHook then - -- delay calling this for .1 seconds so that it hopefully comes after the BIRTH event of the group. - self.SpawnHookScheduler = SCHEDULER:New() - self.SpawnHookScheduler:Schedule( nil, self.SpawnFunctionHook, { self.SpawnGroups[self.SpawnIndex].Group, unpack( self.SpawnFunctionArguments)}, 0.1 ) - -- self.SpawnFunctionHook( self.SpawnGroups[self.SpawnIndex].Group, unpack( self.SpawnFunctionArguments ) ) - end - -- TODO: Need to fix this by putting an "R" in the name of the group when the group repeats. - --if self.Repeat then - -- _DATABASE:SetStatusGroup( SpawnTemplate.name, "ReSpawn" ) - --end - end - - self.SpawnGroups[self.SpawnIndex].Spawned = true - return self.SpawnGroups[self.SpawnIndex].Group - else - --self:E( { self.SpawnTemplatePrefix, "No more Groups to Spawn:", SpawnIndex, self.SpawnMaxGroups } ) - end - - return nil -end - ---- Spawns new groups at varying time intervals. --- This is useful if you want to have continuity within your missions of certain (AI) groups to be present (alive) within your missions. --- @param #SPAWN self --- @param #number SpawnTime The time interval defined in seconds between each new spawn of new groups. --- @param #number SpawnTimeVariation The variation to be applied on the defined time interval between each new spawn. --- The variation is a number between 0 and 1, representing the %-tage of variation to be applied on the time interval. --- @return #SPAWN self --- @usage --- -- NATO helicopters engaging in the battle field. --- -- The time interval is set to SPAWN new helicopters between each 600 seconds, with a time variation of 50%. --- -- The time variation in this case will be between 450 seconds and 750 seconds. --- -- This is calculated as follows: --- -- Low limit: 600 * ( 1 - 0.5 / 2 ) = 450 --- -- High limit: 600 * ( 1 + 0.5 / 2 ) = 750 --- -- Between these two values, a random amount of seconds will be choosen for each new spawn of the helicopters. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Schedule( 600, 0.5 ) -function SPAWN:SpawnScheduled( SpawnTime, SpawnTimeVariation ) - self:F( { SpawnTime, SpawnTimeVariation } ) - - if SpawnTime ~= nil and SpawnTimeVariation ~= nil then - local InitialDelay = 0 - if self.DelayOnOff == true then - InitialDelay = math.random( SpawnTime - SpawnTime * SpawnTimeVariation, SpawnTime + SpawnTime * SpawnTimeVariation ) - end - self.SpawnScheduler = SCHEDULER:New( self, self._Scheduler, {}, InitialDelay, SpawnTime, SpawnTimeVariation ) - end - - return self -end - ---- Will re-start the spawning scheduler. --- Note: This method is only required to be called when the schedule was stopped. --- @param #SPAWN self --- @return #SPAWN -function SPAWN:SpawnScheduleStart() - self:F( { self.SpawnTemplatePrefix } ) - - self.SpawnScheduler:Start() - return self -end - ---- Will stop the scheduled spawning scheduler. --- @param #SPAWN self --- @return #SPAWN -function SPAWN:SpawnScheduleStop() - self:F( { self.SpawnTemplatePrefix } ) - - self.SpawnScheduler:Stop() - return self -end - - ---- Allows to place a CallFunction hook when a new group spawns. --- The provided method will be called when a new group is spawned, including its given parameters. --- The first parameter of the SpawnFunction is the @{Group#GROUP} that was spawned. --- @param #SPAWN self --- @param #function SpawnCallBackFunction The function to be called when a group spawns. --- @param SpawnFunctionArguments A random amount of arguments to be provided to the function when the group spawns. --- @return #SPAWN --- @usage --- -- Declare SpawnObject and call a function when a new Group is spawned. --- local SpawnObject = SPAWN --- :New( "SpawnObject" ) --- :InitLimit( 2, 10 ) --- :OnSpawnGroup( --- function( SpawnGroup ) --- SpawnGroup:E( "I am spawned" ) --- end --- ) --- :SpawnScheduled( 300, 0.3 ) -function SPAWN:OnSpawnGroup( SpawnCallBackFunction, ... ) - self:F( "OnSpawnGroup" ) - - self.SpawnFunctionHook = SpawnCallBackFunction - self.SpawnFunctionArguments = {} - if arg then - self.SpawnFunctionArguments = arg - end - - return self -end - ---- Will spawn a group at an @{Airbase}. --- This method is mostly advisable to be used if you want to simulate spawning units at an airbase. --- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. --- You can use the returned group to further define the route to be followed. --- --- The @{Airbase#AIRBASE} object must refer to a valid airbase known in the sim. --- You can use the following enumerations to search for the pre-defined airbases on the current known maps of DCS: --- --- * @{Airbase#AIRBASE.Caucasus}: The airbases on the Caucasus map. --- * @{Airbase#AIRBASE.Nevada}: The airbases on the Nevada (NTTR) map. --- * @{Airbase#AIRBASE.Normandy}: The airbases on the Normandy map. --- --- Use the method @{Airbase#AIRBASE.FindByName}() to retrieve the airbase object. --- The known AIRBASE objects are automatically imported at mission start by MOOSE. --- Therefore, there isn't any New() constructor defined for AIRBASE objects. --- --- Ships and Farps are added within the mission, and are therefore not known. --- For these AIRBASE objects, there isn't an @{Airbase#AIRBASE} enumeration defined. --- You need to provide the **exact name** of the airbase as the parameter to the @{Airbase#AIRBASE.FindByName}() method! --- --- @param #SPAWN self --- @param Wrapper.Airbase#AIRBASE SpawnAirbase The @{Airbase} where to spawn the group. --- @param #SPAWN.Takeoff Takeoff (optional) The location and takeoff method. Default is Hot. --- @param #number TakeoffAltitude (optional) The altitude above the ground. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil Nothing was spawned. --- @usage --- Spawn_Plane = SPAWN:New( "Plane" ) --- Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), SPAWN.Takeoff.Cold ) --- Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), SPAWN.Takeoff.Hot ) --- Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), SPAWN.Takeoff.Runway ) --- --- Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( "Carrier" ), SPAWN.Takeoff.Cold ) --- --- Spawn_Heli = SPAWN:New( "Heli") --- --- Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "FARP Cold" ), SPAWN.Takeoff.Cold ) --- Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "FARP Hot" ), SPAWN.Takeoff.Hot ) --- Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "FARP Runway" ), SPAWN.Takeoff.Runway ) --- Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "FARP Air" ), SPAWN.Takeoff.Air ) --- --- Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "Carrier" ), SPAWN.Takeoff.Cold ) --- -function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude ) -- R2.2 - self:E( { self.SpawnTemplatePrefix, SpawnAirbase, Takeoff, TakeoffAltitude } ) - - local PointVec3 = SpawnAirbase:GetPointVec3() - self:T2(PointVec3) - - Takeoff = Takeoff or SPAWN.Takeoff.Hot - - if self:_GetSpawnIndex( self.SpawnIndex + 1 ) then - - local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate - - if SpawnTemplate then - - self:T( { "Current point of ", self.SpawnTemplatePrefix, SpawnAirbase } ) - - local SpawnPoint = SpawnTemplate.route.points[1] - - -- These are only for ships. - SpawnPoint.linkUnit = nil - SpawnPoint.helipadId = nil - SpawnPoint.airdromeId = nil - - local AirbaseID = SpawnAirbase:GetID() - local AirbaseCategory = SpawnAirbase:GetDesc().category - self:F( { AirbaseCategory = AirbaseCategory } ) - - if AirbaseCategory == Airbase.Category.SHIP then - SpawnPoint.linkUnit = AirbaseID - SpawnPoint.helipadId = AirbaseID - elseif AirbaseCategory == Airbase.Category.HELIPAD then - SpawnPoint.linkUnit = AirbaseID - SpawnPoint.helipadId = AirbaseID - elseif AirbaseCategory == Airbase.Category.AIRDROME then - SpawnPoint.airdromeId = AirbaseID - end - - SpawnPoint.alt = 0 - - SpawnPoint.type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type - SpawnPoint.action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action - - - -- Translate the position of the Group Template to the Vec3. - for UnitID = 1, #SpawnTemplate.units do - self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - - -- These cause a lot of confusion. - local UnitTemplate = SpawnTemplate.units[UnitID] - - UnitTemplate.parking = nil - UnitTemplate.parking_id = nil - UnitTemplate.alt = 0 - - local SX = UnitTemplate.x - local SY = UnitTemplate.y - local BX = SpawnPoint.x - local BY = SpawnPoint.y - local TX = PointVec3.x + ( SX - BX ) - local TY = PointVec3.z + ( SY - BY ) - - UnitTemplate.x = TX - UnitTemplate.y = TY - - if Takeoff == GROUP.Takeoff.Air then - UnitTemplate.alt = PointVec3.y + ( TakeoffAltitude or 200 ) - --else - -- UnitTemplate.alt = PointVec3.y + 10 - end - self:T( 'After Translation SpawnTemplate.units['..UnitID..'].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. UnitTemplate.y ) - end - - SpawnPoint.x = PointVec3.x - SpawnPoint.y = PointVec3.z - - if Takeoff == GROUP.Takeoff.Air then - SpawnPoint.alt = PointVec3.y + ( TakeoffAltitude or 200 ) - --else - -- SpawnPoint.alt = PointVec3.y + 10 - end - - SpawnTemplate.x = PointVec3.x - SpawnTemplate.y = PointVec3.z - - local GroupSpawned = self:SpawnWithIndex( self.SpawnIndex ) - - -- When spawned in the air, we need to generate a Takeoff Event - - if Takeoff == GROUP.Takeoff.Air then - for UnitID, UnitSpawned in pairs( GroupSpawned:GetUnits() ) do - SCHEDULER:New( nil, BASE.CreateEventTakeoff, { GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject() } , 1 ) - end - end - - return GroupSpawned - end - end - - return nil -end - - - ---- Will spawn a group from a Vec3 in 3D space. --- This method is mostly advisable to be used if you want to simulate spawning units in the air, like helicopters or airplanes. --- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. --- You can use the returned group to further define the route to be followed. --- @param #SPAWN self --- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 coordinates where to spawn the group. --- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil Nothing was spawned. -function SPAWN:SpawnFromVec3( Vec3, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, Vec3, SpawnIndex } ) - - local PointVec3 = POINT_VEC3:NewFromVec3( Vec3 ) - self:T2(PointVec3) - - if SpawnIndex then - else - SpawnIndex = self.SpawnIndex + 1 - end - - if self:_GetSpawnIndex( SpawnIndex ) then - - local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate - - if SpawnTemplate then - - self:T( { "Current point of ", self.SpawnTemplatePrefix, Vec3 } ) - - -- Translate the position of the Group Template to the Vec3. - for UnitID = 1, #SpawnTemplate.units do - self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - local UnitTemplate = SpawnTemplate.units[UnitID] - local SX = UnitTemplate.x - local SY = UnitTemplate.y - local BX = SpawnTemplate.route.points[1].x - local BY = SpawnTemplate.route.points[1].y - local TX = Vec3.x + ( SX - BX ) - local TY = Vec3.z + ( SY - BY ) - SpawnTemplate.units[UnitID].x = TX - SpawnTemplate.units[UnitID].y = TY - SpawnTemplate.units[UnitID].alt = Vec3.y - self:T( 'After Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - end - - SpawnTemplate.route.points[1].x = Vec3.x - SpawnTemplate.route.points[1].y = Vec3.z - SpawnTemplate.route.points[1].alt = Vec3.y - - SpawnTemplate.x = Vec3.x - SpawnTemplate.y = Vec3.z - - return self:SpawnWithIndex( self.SpawnIndex ) - end - end - - return nil -end - ---- Will spawn a group from a Vec2 in 3D space. --- This method is mostly advisable to be used if you want to simulate spawning groups on the ground from air units, like vehicles. --- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. --- You can use the returned group to further define the route to be followed. --- @param #SPAWN self --- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 coordinates where to spawn the group. --- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil Nothing was spawned. -function SPAWN:SpawnFromVec2( Vec2, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, Vec2, SpawnIndex } ) - - local PointVec2 = POINT_VEC2:NewFromVec2( Vec2 ) - return self:SpawnFromVec3( PointVec2:GetVec3(), SpawnIndex ) -end - - ---- Will spawn a group from a hosting unit. This method is mostly advisable to be used if you want to simulate spawning from air units, like helicopters, which are dropping infantry into a defined Landing Zone. --- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. --- You can use the returned group to further define the route to be followed. --- @param #SPAWN self --- @param Wrapper.Unit#UNIT HostUnit The air or ground unit dropping or unloading the group. --- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil Nothing was spawned. -function SPAWN:SpawnFromUnit( HostUnit, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, HostUnit, SpawnIndex } ) - - if HostUnit and HostUnit:IsAlive() ~= nil then -- and HostUnit:getUnit(1):inAir() == false then - return self:SpawnFromVec3( HostUnit:GetVec3(), SpawnIndex ) - end - - return nil -end - ---- Will spawn a group from a hosting static. This method is mostly advisable to be used if you want to simulate spawning from buldings and structures (static buildings). --- You can use the returned group to further define the route to be followed. --- @param #SPAWN self --- @param Wrapper.Static#STATIC HostStatic The static dropping or unloading the group. --- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil Nothing was spawned. -function SPAWN:SpawnFromStatic( HostStatic, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, HostStatic, SpawnIndex } ) - - if HostStatic and HostStatic:IsAlive() then - return self:SpawnFromVec3( HostStatic:GetVec3(), SpawnIndex ) - end - - return nil -end - ---- Will spawn a Group within a given @{Zone}. --- The @{Zone} can be of any type derived from @{Zone#ZONE_BASE}. --- Once the @{Group} is spawned within the zone, the @{Group} will continue on its route. --- The **first waypoint** (where the group is spawned) is replaced with the zone location coordinates. --- @param #SPAWN self --- @param Core.Zone#ZONE Zone The zone where the group is to be spawned. --- @param #boolean RandomizeGroup (optional) Randomization of the @{Group} position in the zone. --- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil when nothing was spawned. -function SPAWN:SpawnInZone( Zone, RandomizeGroup, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, Zone, RandomizeGroup, SpawnIndex } ) - - if Zone then - if RandomizeGroup then - return self:SpawnFromVec2( Zone:GetRandomVec2(), SpawnIndex ) - else - return self:SpawnFromVec2( Zone:GetVec2(), SpawnIndex ) - end - end - - return nil -end - ---- (**AIR**) Will spawn a plane group in UnControlled or Controlled mode... --- This will be similar to the uncontrolled flag setting in the ME. --- You can use UnControlled mode to simulate planes startup and ready for take-off but aren't moving (yet). --- ReSpawn the plane in Controlled mode, and the plane will move... --- @param #SPAWN self --- @param #boolean UnControlled true if UnControlled, false if Controlled. --- @return #SPAWN self -function SPAWN:InitUnControlled( UnControlled ) - self:F2( { self.SpawnTemplatePrefix, UnControlled } ) - - self.SpawnUnControlled = UnControlled - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self.SpawnGroups[SpawnGroupID].UnControlled = UnControlled - end - - return self -end - - ---- Get the Coordinate of the Group that is Late Activated as the template for the SPAWN object. --- @param #SPAWN self --- @return Core.Point#COORDINATE The Coordinate -function SPAWN:GetCoordinate() - - local LateGroup = GROUP:FindByName( self.SpawnTemplatePrefix ) - if LateGroup then - return LateGroup:GetCoordinate() - end - - return nil -end - - ---- Will return the SpawnGroupName either with with a specific count number or without any count. --- @param #SPAWN self --- @param #number SpawnIndex Is the number of the Group that is to be spawned. --- @return #string SpawnGroupName -function SPAWN:SpawnGroupName( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) - - local SpawnPrefix = self.SpawnTemplatePrefix - if self.SpawnAliasPrefix then - SpawnPrefix = self.SpawnAliasPrefix - end - - if SpawnIndex then - local SpawnName = string.format( '%s#%03d', SpawnPrefix, SpawnIndex ) - self:T( SpawnName ) - return SpawnName - else - self:T( SpawnPrefix ) - return SpawnPrefix - end - -end - ---- Will find the first alive @{Group} it has spawned, and return the alive @{Group} object and the first Index where the first alive @{Group} object has been found. --- @param #SPAWN self --- @return Wrapper.Group#GROUP, #number The @{Group} object found, the new Index where the group was found. --- @return #nil, #nil When no group is found, #nil is returned. --- @usage --- -- Find the first alive @{Group} object of the SpawnPlanes SPAWN object @{Group} collection that it has spawned during the mission. --- local GroupPlane, Index = SpawnPlanes:GetFirstAliveGroup() --- while GroupPlane ~= nil do --- -- Do actions with the GroupPlane object. --- GroupPlane, Index = SpawnPlanes:GetNextAliveGroup( Index ) --- end -function SPAWN:GetFirstAliveGroup() - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) - - for SpawnIndex = 1, self.SpawnCount do - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup and SpawnGroup:IsAlive() then - return SpawnGroup, SpawnIndex - end - end - - return nil, nil -end - - ---- Will find the next alive @{Group} object from a given Index, and return a reference to the alive @{Group} object and the next Index where the alive @{Group} has been found. --- @param #SPAWN self --- @param #number SpawnIndexStart A Index holding the start position to search from. This method can also be used to find the first alive @{Group} object from the given Index. --- @return Wrapper.Group#GROUP, #number The next alive @{Group} object found, the next Index where the next alive @{Group} object was found. --- @return #nil, #nil When no alive @{Group} object is found from the start Index position, #nil is returned. --- @usage --- -- Find the first alive @{Group} object of the SpawnPlanes SPAWN object @{Group} collection that it has spawned during the mission. --- local GroupPlane, Index = SpawnPlanes:GetFirstAliveGroup() --- while GroupPlane ~= nil do --- -- Do actions with the GroupPlane object. --- GroupPlane, Index = SpawnPlanes:GetNextAliveGroup( Index ) --- end -function SPAWN:GetNextAliveGroup( SpawnIndexStart ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndexStart } ) - - SpawnIndexStart = SpawnIndexStart + 1 - for SpawnIndex = SpawnIndexStart, self.SpawnCount do - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup and SpawnGroup:IsAlive() then - return SpawnGroup, SpawnIndex - end - end - - return nil, nil -end - ---- Will find the last alive @{Group} object, and will return a reference to the last live @{Group} object and the last Index where the last alive @{Group} object has been found. --- @param #SPAWN self --- @return Wrapper.Group#GROUP, #number The last alive @{Group} object found, the last Index where the last alive @{Group} object was found. --- @return #nil, #nil When no alive @{Group} object is found, #nil is returned. --- @usage --- -- Find the last alive @{Group} object of the SpawnPlanes SPAWN object @{Group} collection that it has spawned during the mission. --- local GroupPlane, Index = SpawnPlanes:GetLastAliveGroup() --- if GroupPlane then -- GroupPlane can be nil!!! --- -- Do actions with the GroupPlane object. --- end -function SPAWN:GetLastAliveGroup() - self:F( { self.SpawnTemplatePrefixself.SpawnAliasPrefix } ) - - self.SpawnIndex = self:_GetLastIndex() - for SpawnIndex = self.SpawnIndex, 1, -1 do - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup and SpawnGroup:IsAlive() then - self.SpawnIndex = SpawnIndex - return SpawnGroup - end - end - - self.SpawnIndex = nil - return nil -end - - - ---- Get the group from an index. --- Returns the group from the SpawnGroups list. --- If no index is given, it will return the first group in the list. --- @param #SPAWN self --- @param #number SpawnIndex The index of the group to return. --- @return Wrapper.Group#GROUP self -function SPAWN:GetGroupFromIndex( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) - - if not SpawnIndex then - SpawnIndex = 1 - end - - if self.SpawnGroups and self.SpawnGroups[SpawnIndex] then - local SpawnGroup = self.SpawnGroups[SpawnIndex].Group - return SpawnGroup - else - return nil - end -end - - ---- Return the prefix of a SpawnUnit. --- The method will search for a #-mark, and will return the text before the #-mark. --- It will return nil of no prefix was found. --- @param #SPAWN self --- @param Dcs.DCSWrapper.Unit#UNIT DCSUnit The @{DCSUnit} to be searched. --- @return #string The prefix --- @return #nil Nothing found -function SPAWN:_GetPrefixFromGroup( SpawnGroup ) - self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) - - local GroupName = SpawnGroup:GetName() - if GroupName then - local SpawnPrefix = string.match( GroupName, ".*#" ) - if SpawnPrefix then - SpawnPrefix = SpawnPrefix:sub( 1, -2 ) - end - return SpawnPrefix - end - - return nil -end - - ---- Get the index from a given group. --- The function will search the name of the group for a #, and will return the number behind the #-mark. -function SPAWN:GetSpawnIndexFromGroup( SpawnGroup ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) - - local IndexString = string.match( SpawnGroup:GetName(), "#(%d*)$" ):sub( 2 ) - local Index = tonumber( IndexString ) - - self:T3( IndexString, Index ) - return Index - -end - ---- Return the last maximum index that can be used. -function SPAWN:_GetLastIndex() - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) - - return self.SpawnMaxGroups -end - ---- Initalize the SpawnGroups collection. --- @param #SPAWN self -function SPAWN:_InitializeSpawnGroups( SpawnIndex ) - self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) - - if not self.SpawnGroups[SpawnIndex] then - self.SpawnGroups[SpawnIndex] = {} - self.SpawnGroups[SpawnIndex].Visible = false - self.SpawnGroups[SpawnIndex].Spawned = false - self.SpawnGroups[SpawnIndex].UnControlled = false - self.SpawnGroups[SpawnIndex].SpawnTime = 0 - - self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefix - self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) - end - - self:_RandomizeTemplate( SpawnIndex ) - self:_RandomizeRoute( SpawnIndex ) - --self:_TranslateRotate( SpawnIndex ) - - return self.SpawnGroups[SpawnIndex] -end - - - ---- Gets the CategoryID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCategoryID( SpawnPrefix ) - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - return TemplateGroup:getCategory() - else - return nil - end -end - ---- Gets the CoalitionID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCoalitionID( SpawnPrefix ) - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - return TemplateGroup:getCoalition() - else - return nil - end -end - ---- Gets the CountryID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCountryID( SpawnPrefix ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnPrefix } ) - - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - local TemplateUnits = TemplateGroup:getUnits() - return TemplateUnits[1]:getCountry() - else - return nil - end -end - ---- Gets the Group Template from the ME environment definition. --- This method used the @{DATABASE} object, which contains ALL initial and new spawned object in MOOSE. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix --- @return @SPAWN self -function SPAWN:_GetTemplate( SpawnTemplatePrefix ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnTemplatePrefix } ) - - local SpawnTemplate = nil - - SpawnTemplate = routines.utils.deepCopy( _DATABASE.Templates.Groups[SpawnTemplatePrefix].Template ) - - if SpawnTemplate == nil then - error( 'No Template returned for SpawnTemplatePrefix = ' .. SpawnTemplatePrefix ) - end - - --SpawnTemplate.SpawnCoalitionID = self:_GetGroupCoalitionID( SpawnTemplatePrefix ) - --SpawnTemplate.SpawnCategoryID = self:_GetGroupCategoryID( SpawnTemplatePrefix ) - --SpawnTemplate.SpawnCountryID = self:_GetGroupCountryID( SpawnTemplatePrefix ) - - self:T3( { SpawnTemplate } ) - return SpawnTemplate -end - ---- Prepares the new Group Template. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) --R2.2 - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) - - local SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) - SpawnTemplate.name = self:SpawnGroupName( SpawnIndex ) - - SpawnTemplate.groupId = nil - --SpawnTemplate.lateActivation = false - SpawnTemplate.lateActivation = false - - if SpawnTemplate.CategoryID == Group.Category.GROUND then - self:T3( "For ground units, visible needs to be false..." ) - SpawnTemplate.visible = false - end - - if self.SpawnGrouping then - local UnitAmount = #SpawnTemplate.units - self:F( { UnitAmount = UnitAmount, SpawnGrouping = self.SpawnGrouping } ) - if UnitAmount > self.SpawnGrouping then - for UnitID = self.SpawnGrouping + 1, UnitAmount do - SpawnTemplate.units[UnitID] = nil - end - else - if UnitAmount < self.SpawnGrouping then - for UnitID = UnitAmount + 1, self.SpawnGrouping do - SpawnTemplate.units[UnitID] = UTILS.DeepCopy( SpawnTemplate.units[1] ) - SpawnTemplate.units[UnitID].unitId = nil - end - end - end - end - - if self.SpawnInitKeepUnitNames == false then - for UnitID = 1, #SpawnTemplate.units do - SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID ) - SpawnTemplate.units[UnitID].unitId = nil - end - else - for UnitID = 1, #SpawnTemplate.units do - local UnitPrefix, Rest = string.match( SpawnTemplate.units[UnitID].name, "^([^#]+)#?" ):gsub( "^%s*(.-)%s*$", "%1" ) - self:T( { UnitPrefix, Rest } ) - - SpawnTemplate.units[UnitID].name = string.format( '%s#%03d-%02d', UnitPrefix, SpawnIndex, UnitID ) - SpawnTemplate.units[UnitID].unitId = nil - end - end - - self:T3( { "Template:", SpawnTemplate } ) - return SpawnTemplate - -end - ---- Private method randomizing the routes. --- @param #SPAWN self --- @param #number SpawnIndex The index of the group to be spawned. --- @return #SPAWN -function SPAWN:_RandomizeRoute( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeRoute, self.SpawnRandomizeRouteStartPoint, self.SpawnRandomizeRouteEndPoint, self.SpawnRandomizeRouteRadius } ) - - if self.SpawnRandomizeRoute then - local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate - local RouteCount = #SpawnTemplate.route.points - - for t = self.SpawnRandomizeRouteStartPoint + 1, ( RouteCount - self.SpawnRandomizeRouteEndPoint ) do - - SpawnTemplate.route.points[t].x = SpawnTemplate.route.points[t].x + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) - SpawnTemplate.route.points[t].y = SpawnTemplate.route.points[t].y + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) - - -- Manage randomization of altitude for airborne units ... - if SpawnTemplate.CategoryID == Group.Category.AIRPLANE or SpawnTemplate.CategoryID == Group.Category.HELICOPTER then - if SpawnTemplate.route.points[t].alt and self.SpawnRandomizeRouteHeight then - SpawnTemplate.route.points[t].alt = SpawnTemplate.route.points[t].alt + math.random( 1, self.SpawnRandomizeRouteHeight ) - end - else - SpawnTemplate.route.points[t].alt = nil - end - - self:T( 'SpawnTemplate.route.points[' .. t .. '].x = ' .. SpawnTemplate.route.points[t].x .. ', SpawnTemplate.route.points[' .. t .. '].y = ' .. SpawnTemplate.route.points[t].y ) - end - end - - self:_RandomizeZones( SpawnIndex ) - - return self -end - ---- Private method that randomizes the template of the group. --- @param #SPAWN self --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_RandomizeTemplate( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeTemplate } ) - - if self.SpawnRandomizeTemplate then - self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefixTable[ math.random( 1, #self.SpawnTemplatePrefixTable ) ] - self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.route = routines.utils.deepCopy( self.SpawnTemplate.route ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.x = self.SpawnTemplate.x - self.SpawnGroups[SpawnIndex].SpawnTemplate.y = self.SpawnTemplate.y - self.SpawnGroups[SpawnIndex].SpawnTemplate.start_time = self.SpawnTemplate.start_time - local OldX = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].x - local OldY = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].y - for UnitID = 1, #self.SpawnGroups[SpawnIndex].SpawnTemplate.units do - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].heading = self.SpawnTemplate.units[1].heading - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x = self.SpawnTemplate.units[1].x + ( self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x - OldX ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y = self.SpawnTemplate.units[1].y + ( self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y - OldY ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].alt = self.SpawnTemplate.units[1].alt - end - end - - self:_RandomizeRoute( SpawnIndex ) - - return self -end - ---- Private method that randomizes the @{Zone}s where the Group will be spawned. --- @param #SPAWN self --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_RandomizeZones( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeZones } ) - - if self.SpawnRandomizeZones then - local SpawnZone = nil -- Core.Zone#ZONE_BASE - while not SpawnZone do - self:T( { SpawnZoneTableCount = #self.SpawnZoneTable, self.SpawnZoneTable } ) - local ZoneID = math.random( #self.SpawnZoneTable ) - self:T( ZoneID ) - SpawnZone = self.SpawnZoneTable[ ZoneID ]:GetZoneMaybe() - end - - self:T( "Preparing Spawn in Zone", SpawnZone:GetName() ) - - local SpawnVec2 = SpawnZone:GetRandomVec2() - - self:T( { SpawnVec2 = SpawnVec2 } ) - - local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate - - self:T( { Route = SpawnTemplate.route } ) - - for UnitID = 1, #SpawnTemplate.units do - local UnitTemplate = SpawnTemplate.units[UnitID] - self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. UnitTemplate.y ) - local SX = UnitTemplate.x - local SY = UnitTemplate.y - local BX = SpawnTemplate.route.points[1].x - local BY = SpawnTemplate.route.points[1].y - local TX = SpawnVec2.x + ( SX - BX ) - local TY = SpawnVec2.y + ( SY - BY ) - UnitTemplate.x = TX - UnitTemplate.y = TY - -- TODO: Manage altitude based on landheight... - --SpawnTemplate.units[UnitID].alt = SpawnVec2: - self:T( 'After Translation SpawnTemplate.units['..UnitID..'].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. UnitTemplate.y ) - end - SpawnTemplate.x = SpawnVec2.x - SpawnTemplate.y = SpawnVec2.y - SpawnTemplate.route.points[1].x = SpawnVec2.x - SpawnTemplate.route.points[1].y = SpawnVec2.y - end - - return self - -end - -function SPAWN:_TranslateRotate( SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle } ) - - -- Translate - local TranslatedX = SpawnX - local TranslatedY = SpawnY - - -- Rotate - -- From Wikipedia: https://en.wikipedia.org/wiki/Rotation_matrix#Common_rotations - -- x' = x \cos \theta - y \sin \theta\ - -- y' = x \sin \theta + y \cos \theta\ - local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) - + TranslatedY * math.sin( math.rad( SpawnAngle ) ) - local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) - + TranslatedY * math.cos( math.rad( SpawnAngle ) ) - - -- Assign - self.SpawnGroups[SpawnIndex].SpawnTemplate.x = SpawnRootX - RotatedX - self.SpawnGroups[SpawnIndex].SpawnTemplate.y = SpawnRootY + RotatedY - - - local SpawnUnitCount = table.getn( self.SpawnGroups[SpawnIndex].SpawnTemplate.units ) - for u = 1, SpawnUnitCount do - - -- Translate - local TranslatedX = SpawnX - local TranslatedY = SpawnY - 10 * ( u - 1 ) - - -- Rotate - local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) - + TranslatedY * math.sin( math.rad( SpawnAngle ) ) - local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) - + TranslatedY * math.cos( math.rad( SpawnAngle ) ) - - -- Assign - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].x = SpawnRootX - RotatedX - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].y = SpawnRootY + RotatedY - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading + math.rad( SpawnAngle ) - end - - return self -end - ---- Get the next index of the groups to be spawned. This method is complicated, as it is used at several spaces. -function SPAWN:_GetSpawnIndex( SpawnIndex ) - self:F2( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive, self.AliveUnits, #self.SpawnTemplate.units } ) - - if ( self.SpawnMaxGroups == 0 ) or ( SpawnIndex <= self.SpawnMaxGroups ) then - if ( self.SpawnMaxUnitsAlive == 0 ) or ( self.AliveUnits + #self.SpawnTemplate.units <= self.SpawnMaxUnitsAlive ) or self.UnControlled == true then - if SpawnIndex and SpawnIndex >= self.SpawnCount + 1 then - self.SpawnCount = self.SpawnCount + 1 - SpawnIndex = self.SpawnCount - end - self.SpawnIndex = SpawnIndex - if not self.SpawnGroups[self.SpawnIndex] then - self:_InitializeSpawnGroups( self.SpawnIndex ) - end - else - return nil - end - else - return nil - end - - return self.SpawnIndex -end - - --- TODO Need to delete this... _DATABASE does this now ... - ---- @param #SPAWN self --- @param Core.Event#EVENTDATA EventData -function SPAWN:_OnBirth( EventData ) - self:F( self.SpawnTemplatePrefix ) - - local SpawnGroup = EventData.IniGroup - - if SpawnGroup then - local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) - if EventPrefix then -- EventPrefix can be nil if no # is found, which means, no spawnable group! - self:T( { "Birth Event:", EventPrefix, self.SpawnTemplatePrefix } ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - self.AliveUnits = self.AliveUnits + 1 - self:T( "Alive Units: " .. self.AliveUnits ) - end - end - end - -end - ---- Obscolete --- @todo Need to delete this... _DATABASE does this now ... - ---- @param #SPAWN self --- @param Core.Event#EVENTDATA EventData -function SPAWN:_OnDeadOrCrash( EventData ) - self:F( self.SpawnTemplatePrefix ) - - local SpawnGroup = EventData.IniGroup - - if SpawnGroup then - local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) - if EventPrefix then -- EventPrefix can be nil if no # is found, which means, no spawnable group! - self:T( { "Dead event: " .. EventPrefix } ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - self.AliveUnits = self.AliveUnits - 1 - self:T( "Alive Units: " .. self.AliveUnits ) - end - end - end -end - ---- Will detect AIR Units taking off... When the event takes place, the spawned Group is registered as airborne... --- This is needed to ensure that Re-SPAWNing only is done for landed AIR Groups. --- @param #SPAWN self --- @param Core.Event#EVENTDATA EventData -function SPAWN:_OnTakeOff( EventData ) - self:F( self.SpawnTemplatePrefix ) - - local SpawnGroup = EventData.IniGroup - if SpawnGroup then - local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) - if EventPrefix then -- EventPrefix can be nil if no # is found, which means, no spawnable group! - self:T( { "TakeOff event: " .. EventPrefix } ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - self:T( "self.Landed = false" ) - SpawnGroup:SetState( SpawnGroup, "Spawn_Landed", false ) - end - end - end -end - ---- Will detect AIR Units landing... When the event takes place, the spawned Group is registered as landed. --- This is needed to ensure that Re-SPAWNing is only done for landed AIR Groups. --- @param #SPAWN self --- @param Core.Event#EVENTDATA EventData -function SPAWN:_OnLand( EventData ) - self:F( self.SpawnTemplatePrefix ) - - local SpawnGroup = EventData.IniGroup - if SpawnGroup then - local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) - if EventPrefix then -- EventPrefix can be nil if no # is found, which means, no spawnable group! - self:T( { "Land event: " .. EventPrefix } ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - -- TODO: Check if this is the last unit of the group that lands. - SpawnGroup:SetState( SpawnGroup, "Spawn_Landed", true ) - if self.RepeatOnLanding then - local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) - self:T( { "Landed:", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) - self:ReSpawn( SpawnGroupIndex ) - end - end - end - end -end - ---- Will detect AIR Units shutting down their engines ... --- When the event takes place, and the method @{RepeatOnEngineShutDown} was called, the spawned Group will Re-SPAWN. --- But only when the Unit was registered to have landed. --- @param #SPAWN self --- @param Core.Event#EVENTDATA EventData -function SPAWN:_OnEngineShutDown( EventData ) - self:F( self.SpawnTemplatePrefix ) - - local SpawnGroup = EventData.IniGroup - if SpawnGroup then - local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) - if EventPrefix then -- EventPrefix can be nil if no # is found, which means, no spawnable group! - self:T( { "EngineShutdown event: " .. EventPrefix } ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - -- todo: test if on the runway - local Landed = SpawnGroup:GetState( SpawnGroup, "Spawn_Landed" ) - if Landed and self.RepeatOnEngineShutDown then - local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) - self:T( { "EngineShutDown: ", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) - self:ReSpawn( SpawnGroupIndex ) - end - end - end - end -end - ---- This function is called automatically by the Spawning scheduler. --- It is the internal worker method SPAWNing new Groups on the defined time intervals. -function SPAWN:_Scheduler() - self:F2( { "_Scheduler", self.SpawnTemplatePrefix, self.SpawnAliasPrefix, self.SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive } ) - - -- Validate if there are still groups left in the batch... - self:Spawn() - - return true -end - ---- Schedules the CleanUp of Groups --- @param #SPAWN self --- @return #boolean True = Continue Scheduler -function SPAWN:_SpawnCleanUpScheduler() - self:F( { "CleanUp Scheduler:", self.SpawnTemplatePrefix } ) - - local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup() - self:T( { "CleanUp Scheduler:", SpawnGroup, SpawnCursor } ) - - while SpawnGroup do - - local SpawnUnits = SpawnGroup:GetUnits() - - for UnitID, UnitData in pairs( SpawnUnits ) do - - local SpawnUnit = UnitData -- Wrapper.Unit#UNIT - local SpawnUnitName = SpawnUnit:GetName() - - - self.SpawnCleanUpTimeStamps[SpawnUnitName] = self.SpawnCleanUpTimeStamps[SpawnUnitName] or {} - local Stamp = self.SpawnCleanUpTimeStamps[SpawnUnitName] - self:T( { SpawnUnitName, Stamp } ) - - if Stamp.Vec2 then - if SpawnUnit:InAir() == false and SpawnUnit:GetVelocityKMH() < 1 then - local NewVec2 = SpawnUnit:GetVec2() - if Stamp.Vec2.x == NewVec2.x and Stamp.Vec2.y == NewVec2.y then - -- If the plane is not moving, and is on the ground, assign it with a timestamp... - if Stamp.Time + self.SpawnCleanUpInterval < timer.getTime() then - self:T( { "CleanUp Scheduler:", "ReSpawning:", SpawnGroup:GetName() } ) - self:ReSpawn( SpawnCursor ) - Stamp.Vec2 = nil - Stamp.Time = nil - end - else - Stamp.Time = timer.getTime() - Stamp.Vec2 = SpawnUnit:GetVec2() - end - else - Stamp.Vec2 = nil - Stamp.Time = nil - end - else - if SpawnUnit:InAir() == false then - Stamp.Vec2 = SpawnUnit:GetVec2() - if SpawnUnit:GetVelocityKMH() < 1 then - Stamp.Time = timer.getTime() - end - else - Stamp.Time = nil - Stamp.Vec2 = nil - end - end - end - - SpawnGroup, SpawnCursor = self:GetNextAliveGroup( SpawnCursor ) - - self:T( { "CleanUp Scheduler:", SpawnGroup, SpawnCursor } ) - - end - - return true -- Repeat - -end ---- **Functional** -- Limit the MOVEMENT of simulaneous moving ground vehicles. --- --- ==== --- --- Limit the simultaneous movement of Groups within a running Mission. --- This module is defined to improve the performance in missions, and to bring additional realism for GROUND vehicles. --- Performance: If in a DCSRTE there are a lot of moving GROUND units, then in a multi player mission, this WILL create lag if --- the main DCS execution core of your CPU is fully utilized. So, this class will limit the amount of simultaneous moving GROUND units --- on defined intervals (currently every minute). --- @module Movement - ---- the MOVEMENT class --- @type MOVEMENT --- @extends Core.Base#BASE -MOVEMENT = { - ClassName = "MOVEMENT", -} - ---- Creates the main object which is handling the GROUND forces movement. --- @param table{string,...}|string MovePrefixes is a table of the Prefixes (names) of the GROUND Groups that need to be controlled by the MOVEMENT Object. --- @param number MoveMaximum is a number that defines the maximum amount of GROUND Units to be moving during one minute. --- @return MOVEMENT --- @usage --- -- Limit the amount of simultaneous moving units on the ground to prevent lag. --- Movement_US_Platoons = MOVEMENT:New( { 'US Tank Platoon Left', 'US Tank Platoon Middle', 'US Tank Platoon Right', 'US CH-47D Troops' }, 15 ) - -function MOVEMENT:New( MovePrefixes, MoveMaximum ) - local self = BASE:Inherit( self, BASE:New() ) -- #MOVEMENT - self:F( { MovePrefixes, MoveMaximum } ) - - if type( MovePrefixes ) == 'table' then - self.MovePrefixes = MovePrefixes - else - self.MovePrefixes = { MovePrefixes } - end - self.MoveCount = 0 -- The internal counter of the amount of Moveing the has happened since MoveStart. - self.MoveMaximum = MoveMaximum -- Contains the Maximum amount of units that are allowed to move... - self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.MoveUnits = {} -- Reflects if the Moving for this MovePrefixes is going to be scheduled or not. - - self:HandleEvent( EVENTS.Birth ) - --- self:AddEvent( world.event.S_EVENT_BIRTH, self.OnBirth ) --- --- self:EnableEvents() - - self:ScheduleStart() - - return self -end - ---- Call this function to start the MOVEMENT scheduling. -function MOVEMENT:ScheduleStart() - self:F() - --self.MoveFunction = routines.scheduleFunction( self._Scheduler, { self }, timer.getTime() + 1, 120 ) - self.MoveFunction = SCHEDULER:New( self, self._Scheduler, {}, 1, 120 ) -end - ---- Call this function to stop the MOVEMENT scheduling. --- @todo need to implement it ... Forgot. -function MOVEMENT:ScheduleStop() - self:F() - -end - ---- Captures the birth events when new Units were spawned. --- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration. --- @param #MOVEMENT self --- @param Core.Event#EVENTDATA self -function MOVEMENT:OnEventBirth( EventData ) - self:F( { EventData } ) - - if timer.getTime0() < timer.getAbsTime() then -- dont need to add units spawned in at the start of the mission if mist is loaded in init line - if EventData.IniDCSUnit then - self:T( "Birth object : " .. EventData.IniDCSUnitName ) - if EventData.IniDCSGroup and EventData.IniDCSGroup:isExist() then - for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do - if string.find( EventData.IniDCSUnitName, MovePrefix, 1, true ) then - self.AliveUnits = self.AliveUnits + 1 - self.MoveUnits[EventData.IniDCSUnitName] = EventData.IniDCSGroupName - self:T( self.AliveUnits ) - end - end - end - end - - EventData.IniUnit:HandleEvent( EVENTS.DEAD, self.OnDeadOrCrash ) - end - -end - ---- Captures the Dead or Crash events when Units crash or are destroyed. --- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration. -function MOVEMENT:OnDeadOrCrash( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - self:T( "Dead object : " .. Event.IniDCSUnitName ) - for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do - if string.find( Event.IniDCSUnitName, MovePrefix, 1, true ) then - self.AliveUnits = self.AliveUnits - 1 - self.MoveUnits[Event.IniDCSUnitName] = nil - self:T( self.AliveUnits ) - end - end - end -end - ---- This function is called automatically by the MOVEMENT scheduler. A new function is scheduled when MoveScheduled is true. -function MOVEMENT:_Scheduler() - self:F( { self.MovePrefixes, self.MoveMaximum, self.AliveUnits, self.MovementGroups } ) - - if self.AliveUnits > 0 then - local MoveProbability = ( self.MoveMaximum * 100 ) / self.AliveUnits - self:T( 'Move Probability = ' .. MoveProbability ) - - for MovementUnitName, MovementGroupName in pairs( self.MoveUnits ) do - local MovementGroup = Group.getByName( MovementGroupName ) - if MovementGroup and MovementGroup:isExist() then - local MoveOrStop = math.random( 1, 100 ) - self:T( 'MoveOrStop = ' .. MoveOrStop ) - if MoveOrStop <= MoveProbability then - self:T( 'Group continues moving = ' .. MovementGroupName ) - trigger.action.groupContinueMoving( MovementGroup ) - else - self:T( 'Group stops moving = ' .. MovementGroupName ) - trigger.action.groupStopMoving( MovementGroup ) - end - else - self.MoveUnits[MovementUnitName] = nil - end - end - end - return true -end ---- **Functional** -- Provides defensive behaviour to a set of SAM sites within a running Mission. --- --- ==== --- --- @module Sead - ---- The SEAD class --- @type SEAD --- @extends Core.Base#BASE -SEAD = { - ClassName = "SEAD", - TargetSkill = { - Average = { Evade = 50, DelayOff = { 10, 25 }, DelayOn = { 10, 30 } } , - Good = { Evade = 30, DelayOff = { 8, 20 }, DelayOn = { 20, 40 } } , - High = { Evade = 15, DelayOff = { 5, 17 }, DelayOn = { 30, 50 } } , - Excellent = { Evade = 10, DelayOff = { 3, 10 }, DelayOn = { 30, 60 } } - }, - SEADGroupPrefixes = {} -} - ---- Creates the main object which is handling defensive actions for SA sites or moving SA vehicles. --- When an anti radiation missile is fired (KH-58, KH-31P, KH-31A, KH-25MPU, HARM missiles), the SA will shut down their radars and will take evasive actions... --- Chances are big that the missile will miss. --- @param table{string,...}|string SEADGroupPrefixes which is a table of Prefixes of the SA Groups in the DCSRTE on which evasive actions need to be taken. --- @return SEAD --- @usage --- -- CCCP SEAD Defenses --- -- Defends the Russian SA installations from SEAD attacks. --- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } ) -function SEAD:New( SEADGroupPrefixes ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( SEADGroupPrefixes ) - if type( SEADGroupPrefixes ) == 'table' then - for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do - self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix - end - else - self.SEADGroupNames[SEADGroupPrefixes] = SEADGroupPrefixes - end - - self:HandleEvent( EVENTS.Shot ) - - return self -end - ---- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. --- @see SEAD --- @param #SEAD --- @param Core.Event#EVENTDATA EventData -function SEAD:OnEventShot( EventData ) - self:F( { EventData } ) - - local SEADUnit = EventData.IniDCSUnit - local SEADUnitName = EventData.IniDCSUnitName - local SEADWeapon = EventData.Weapon -- Identify the weapon fired - local SEADWeaponName = EventData.WeaponName -- return weapon type - -- Start of the 2nd loop - self:T( "Missile Launched = " .. SEADWeaponName ) - if SEADWeaponName == "KH-58" or SEADWeaponName == "KH-25MPU" or SEADWeaponName == "AGM-88" or SEADWeaponName == "KH-31A" or SEADWeaponName == "KH-31P" then -- Check if the missile is a SEAD - local _evade = math.random (1,100) -- random number for chance of evading action - local _targetMim = EventData.Weapon:getTarget() -- Identify target - local _targetMimname = Unit.getName(_targetMim) - local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) - local _targetMimgroupName = _targetMimgroup:getName() - local _targetMimcont= _targetMimgroup:getController() - local _targetskill = _DATABASE.Templates.Units[_targetMimname].Template.skill - self:T( self.SEADGroupPrefixes ) - self:T( _targetMimgroupName ) - local SEADGroupFound = false - for SEADGroupPrefixID, SEADGroupPrefix in pairs( self.SEADGroupPrefixes ) do - if string.find( _targetMimgroupName, SEADGroupPrefix, 1, true ) then - SEADGroupFound = true - self:T( 'Group Found' ) - break - end - end - if SEADGroupFound == true then - if _targetskill == "Random" then -- when skill is random, choose a skill - local Skills = { "Average", "Good", "High", "Excellent" } - _targetskill = Skills[ math.random(1,4) ] - end - self:T( _targetskill ) - if self.TargetSkill[_targetskill] then - if (_evade > self.TargetSkill[_targetskill].Evade) then - self:T( string.format("Evading, target skill " ..string.format(_targetskill)) ) - local _targetMim = Weapon.getTarget(SEADWeapon) - local _targetMimname = Unit.getName(_targetMim) - local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) - local _targetMimcont= _targetMimgroup:getController() - routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly - local SuppressedGroups1 = {} -- unit suppressed radar off for a random time - local function SuppressionEnd1(id) - id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) - SuppressedGroups1[id.groupName] = nil - end - local id = { - groupName = _targetMimgroup, - ctrl = _targetMimcont - } - local delay1 = math.random(self.TargetSkill[_targetskill].DelayOff[1], self.TargetSkill[_targetskill].DelayOff[2]) - if SuppressedGroups1[id.groupName] == nil then - SuppressedGroups1[id.groupName] = { - SuppressionEndTime1 = timer.getTime() + delay1, - SuppressionEndN1 = SuppressionEndCounter1 --Store instance of SuppressionEnd() scheduled function - } - Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) - timer.scheduleFunction(SuppressionEnd1, id, SuppressedGroups1[id.groupName].SuppressionEndTime1) --Schedule the SuppressionEnd() function - --trigger.action.outText( string.format("Radar Off " ..string.format(delay1)), 20) - end - - local SuppressedGroups = {} - local function SuppressionEnd(id) - id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) - SuppressedGroups[id.groupName] = nil - end - local id = { - groupName = _targetMimgroup, - ctrl = _targetMimcont - } - local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) - if SuppressedGroups[id.groupName] == nil then - SuppressedGroups[id.groupName] = { - SuppressionEndTime = timer.getTime() + delay, - SuppressionEndN = SuppressionEndCounter --Store instance of SuppressionEnd() scheduled function - } - timer.scheduleFunction(SuppressionEnd, id, SuppressedGroups[id.groupName].SuppressionEndTime) --Schedule the SuppressionEnd() function - --trigger.action.outText( string.format("Radar On " ..string.format(delay)), 20) - end - end - end - end - end -end ---- **Functional** -- Taking the lead of AI escorting your flight. --- --- ==== --- --- @{#ESCORT} class --- ================ --- The @{#ESCORT} class allows you to interact with escorting AI on your flight and take the lead. --- Each escorting group can be commanded with a whole set of radio commands (radio menu in your flight, and then F10). --- --- The radio commands will vary according the category of the group. The richest set of commands are with Helicopters and AirPlanes. --- Ships and Ground troops will have a more limited set, but they can provide support through the bombing of targets designated by the other escorts. --- --- RADIO MENUs that can be created: --- ================================ --- Find a summary below of the current available commands: --- --- Navigation ...: --- --------------- --- Escort group navigation functions: --- --- * **"Join-Up and Follow at x meters":** The escort group fill follow you at about x meters, and they will follow you. --- * **"Flare":** Provides menu commands to let the escort group shoot a flare in the air in a color. --- * **"Smoke":** Provides menu commands to let the escort group smoke the air in a color. Note that smoking is only available for ground and naval troops. --- --- Hold position ...: --- ------------------ --- Escort group navigation functions: --- --- * **"At current location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. --- * **"At client location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. --- --- Report targets ...: --- ------------------- --- Report targets will make the escort group to report any target that it identifies within a 8km range. Any detected target can be attacked using the 4. Attack nearby targets function. (see below). --- --- * **"Report now":** Will report the current detected targets. --- * **"Report targets on":** Will make the escort group to report detected targets and will fill the "Attack nearby targets" menu list. --- * **"Report targets off":** Will stop detecting targets. --- --- Scan targets ...: --- ----------------- --- Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or defined task. --- --- * **"Scan targets 30 seconds":** Scan 30 seconds for targets. --- * **"Scan targets 60 seconds":** Scan 60 seconds for targets. --- --- Attack targets ...: --- ------------------- --- This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed. --- --- Request assistance from ...: --- ---------------------------- --- This menu item will list all detected targets within a 15km range, as with the menu item **Attack Targets**. --- This menu item allows to request attack support from other escorts supporting the current client group. --- eg. the function allows a player to request support from the Ship escort to attack a target identified by the Plane escort with its Tomahawk missiles. --- eg. the function allows a player to request support from other Planes escorting to bomb the unit with illumination missiles or bombs, so that the main plane escort can attack the area. --- --- ROE ...: --- -------- --- Sets the Rules of Engagement (ROE) of the escort group when in flight. --- --- * **"Hold Fire":** The escort group will hold fire. --- * **"Return Fire":** The escort group will return fire. --- * **"Open Fire":** The escort group will open fire on designated targets. --- * **"Weapon Free":** The escort group will engage with any target. --- --- Evasion ...: --- ------------ --- Will define the evasion techniques that the escort group will perform during flight or combat. --- --- * **"Fight until death":** The escort group will have no reaction to threats. --- * **"Use flares, chaff and jammers":** The escort group will use passive defense using flares and jammers. No evasive manoeuvres are executed. --- * **"Evade enemy fire":** The rescort group will evade enemy fire before firing. --- * **"Go below radar and evade fire":** The escort group will perform evasive vertical manoeuvres. --- --- Resume Mission ...: --- ------------------- --- Escort groups can have their own mission. This menu item will allow the escort group to resume their Mission from a given waypoint. --- Note that this is really fantastic, as you now have the dynamic of taking control of the escort groups, and allowing them to resume their path or mission. --- --- ESCORT construction methods. --- ============================ --- Create a new SPAWN object with the @{#ESCORT.New} method: --- --- * @{#ESCORT.New}: Creates a new ESCORT object from a @{Group#GROUP} for a @{Client#CLIENT}, with an optional briefing text. --- --- ESCORT initialization methods. --- ============================== --- The following menus are created within the RADIO MENU (F10) of an active unit hosted by a player: --- --- * @{#ESCORT.MenuFollowAt}: Creates a menu to make the escort follow the client. --- * @{#ESCORT.MenuHoldAtEscortPosition}: Creates a menu to hold the escort at its current position. --- * @{#ESCORT.MenuHoldAtLeaderPosition}: Creates a menu to hold the escort at the client position. --- * @{#ESCORT.MenuScanForTargets}: Creates a menu so that the escort scans targets. --- * @{#ESCORT.MenuFlare}: Creates a menu to disperse flares. --- * @{#ESCORT.MenuSmoke}: Creates a menu to disparse smoke. --- * @{#ESCORT.MenuReportTargets}: Creates a menu so that the escort reports targets. --- * @{#ESCORT.MenuReportPosition}: Creates a menu so that the escort reports its current position from bullseye. --- * @{#ESCORT.MenuAssistedAttack: Creates a menu so that the escort supportes assisted attack from other escorts with the client. --- * @{#ESCORT.MenuROE: Creates a menu structure to set the rules of engagement of the escort. --- * @{#ESCORT.MenuEvasion: Creates a menu structure to set the evasion techniques when the escort is under threat. --- * @{#ESCORT.MenuResumeMission}: Creates a menu structure so that the escort can resume from a waypoint. --- --- --- @usage --- -- Declare a new EscortPlanes object as follows: --- --- -- First find the GROUP object and the CLIENT object. --- local EscortClient = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. --- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. --- --- -- Now use these 2 objects to construct the new EscortPlanes object. --- EscortPlanes = ESCORT:New( EscortClient, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) --- --- --- --- @module Escort --- @author FlightControl - ---- ESCORT class --- @type ESCORT --- @extends Core.Base#BASE --- @field Wrapper.Client#CLIENT EscortClient --- @field Wrapper.Group#GROUP EscortGroup --- @field #string EscortName --- @field #ESCORT.MODE EscortMode The mode the escort is in. --- @field Core.Scheduler#SCHEDULER FollowScheduler The instance of the SCHEDULER class. --- @field #number FollowDistance The current follow distance. --- @field #boolean ReportTargets If true, nearby targets are reported. --- @Field Dcs.DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the EscortGroup. --- @field Dcs.DCSTypes#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the EscortGroup. --- @field Core.Menu#MENU_CLIENT EscortMenuResumeMission --- @field Functional.Detection#DETECTION_BASE Detection -ESCORT = { - ClassName = "ESCORT", - EscortName = nil, -- The Escort Name - EscortClient = nil, - EscortGroup = nil, - EscortMode = 1, - MODE = { - FOLLOW = 1, - MISSION = 2, - }, - Targets = {}, -- The identified targets - FollowScheduler = nil, - ReportTargets = true, - OptionROE = AI.Option.Air.val.ROE.OPEN_FIRE, - OptionReactionOnThreat = AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION, - SmokeDirectionVector = false, - TaskPoints = {} -} - ---- ESCORT.Mode class --- @type ESCORT.MODE --- @field #number FOLLOW --- @field #number MISSION - ---- MENUPARAM type --- @type MENUPARAM --- @field #ESCORT ParamSelf --- @field #Distance ParamDistance --- @field #function ParamFunction --- @field #string ParamMessage - ---- ESCORT class constructor for an AI group --- @param #ESCORT self --- @param Wrapper.Client#CLIENT EscortClient The client escorted by the EscortGroup. --- @param Wrapper.Group#GROUP EscortGroup The group AI escorting the EscortClient. --- @param #string EscortName Name of the escort. --- @param #string EscortBriefing A text showing the ESCORT briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. --- @return #ESCORT self --- @usage --- -- Declare a new EscortPlanes object as follows: --- --- -- First find the GROUP object and the CLIENT object. --- local EscortClient = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. --- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. --- --- -- Now use these 2 objects to construct the new EscortPlanes object. --- EscortPlanes = ESCORT:New( EscortClient, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) -function ESCORT:New( EscortClient, EscortGroup, EscortName, EscortBriefing ) - - local self = BASE:Inherit( self, BASE:New() ) -- #ESCORT - self:F( { EscortClient, EscortGroup, EscortName } ) - - self.EscortClient = EscortClient -- Wrapper.Client#CLIENT - self.EscortGroup = EscortGroup -- Wrapper.Group#GROUP - self.EscortName = EscortName - self.EscortBriefing = EscortBriefing - - self.EscortSetGroup = SET_GROUP:New() - self.EscortSetGroup:AddObject( self.EscortGroup ) - self.EscortSetGroup:Flush() - self.Detection = DETECTION_UNITS:New( self.EscortSetGroup, 15000 ) - - self.EscortGroup.Detection = self.Detection - - -- Set EscortGroup known at EscortClient. - if not self.EscortClient._EscortGroups then - self.EscortClient._EscortGroups = {} - end - - if not self.EscortClient._EscortGroups[EscortGroup:GetName()] then - self.EscortClient._EscortGroups[EscortGroup:GetName()] = {} - self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortGroup = self.EscortGroup - self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortName = self.EscortName - self.EscortClient._EscortGroups[EscortGroup:GetName()].Detection = self.EscortGroup.Detection - end - - self.EscortMenu = MENU_CLIENT:New( self.EscortClient, self.EscortName ) - - self.EscortGroup:WayPointInitialize(1) - - self.EscortGroup:OptionROTVertical() - self.EscortGroup:OptionROEOpenFire() - - if not EscortBriefing then - EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " .. - "We're escorting your flight. " .. - "Use the Radio Menu and F10 and use the options under + " .. EscortName .. "\n", - 60, EscortClient - ) - else - EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") " .. EscortBriefing, - 60, EscortClient - ) - end - - self.FollowDistance = 100 - self.CT1 = 0 - self.GT1 = 0 - - self.FollowScheduler, self.FollowSchedule = SCHEDULER:New( self, self._FollowScheduler, {}, 1, .5, .01 ) - self.FollowScheduler:Stop( self.FollowSchedule ) - - self.EscortMode = ESCORT.MODE.MISSION - - - return self -end - ---- Set a Detection method for the EscortClient to be reported upon. --- Detection methods are based on the derived classes from DETECTION_BASE. --- @param #ESCORT self --- @param Function.Detection#DETECTION_BASE Detection -function ESCORT:SetDetection( Detection ) - - self.Detection = Detection - self.EscortGroup.Detection = self.Detection - self.EscortClient._EscortGroups[self.EscortGroup:GetName()].Detection = self.EscortGroup.Detection - - Detection:__Start( 1 ) - -end - ---- This function is for test, it will put on the frequency of the FollowScheduler a red smoke at the direction vector calculated for the escort to fly to. --- This allows to visualize where the escort is flying to. --- @param #ESCORT self --- @param #boolean SmokeDirection If true, then the direction vector will be smoked. -function ESCORT:TestSmokeDirectionVector( SmokeDirection ) - self.SmokeDirectionVector = ( SmokeDirection == true ) and true or false -end - - ---- Defines the default menus --- @param #ESCORT self --- @return #ESCORT -function ESCORT:Menus() - self:F() - - self:MenuFollowAt( 100 ) - self:MenuFollowAt( 200 ) - self:MenuFollowAt( 300 ) - self:MenuFollowAt( 400 ) - - self:MenuScanForTargets( 100, 60 ) - - self:MenuHoldAtEscortPosition( 30 ) - self:MenuHoldAtLeaderPosition( 30 ) - - self:MenuFlare() - self:MenuSmoke() - - self:MenuReportTargets( 60 ) - self:MenuAssistedAttack() - self:MenuROE() - self:MenuEvasion() - self:MenuResumeMission() - - - return self -end - - - ---- Defines a menu slot to let the escort Join and Follow you at a certain distance. --- This menu will appear under **Navigation**. --- @param #ESCORT self --- @param Dcs.DCSTypes#Distance Distance The distance in meters that the escort needs to follow the client. --- @return #ESCORT -function ESCORT:MenuFollowAt( Distance ) - self:F(Distance) - - if self.EscortGroup:IsAir() then - if not self.EscortMenuReportNavigation then - self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) - end - - if not self.EscortMenuJoinUpAndFollow then - self.EscortMenuJoinUpAndFollow = {} - end - - self.EscortMenuJoinUpAndFollow[#self.EscortMenuJoinUpAndFollow+1] = MENU_CLIENT_COMMAND:New( self.EscortClient, "Join-Up and Follow at " .. Distance, self.EscortMenuReportNavigation, ESCORT._JoinUpAndFollow, self, Distance ) - - self.EscortMode = ESCORT.MODE.FOLLOW - end - - return self -end - ---- Defines a menu slot to let the escort hold at their current position and stay low with a specified height during a specified time in seconds. --- This menu will appear under **Hold position**. --- @param #ESCORT self --- @param Dcs.DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. --- @param Dcs.DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. --- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. --- @return #ESCORT --- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function. -function ESCORT:MenuHoldAtEscortPosition( Height, Seconds, MenuTextFormat ) - self:F( { Height, Seconds, MenuTextFormat } ) - - if self.EscortGroup:IsAir() then - - if not self.EscortMenuHold then - self.EscortMenuHold = MENU_CLIENT:New( self.EscortClient, "Hold position", self.EscortMenu ) - end - - if not Height then - Height = 30 - end - - if not Seconds then - Seconds = 0 - end - - local MenuText = "" - if not MenuTextFormat then - if Seconds == 0 then - MenuText = string.format( "Hold at %d meter", Height ) - else - MenuText = string.format( "Hold at %d meter for %d seconds", Height, Seconds ) - end - else - if Seconds == 0 then - MenuText = string.format( MenuTextFormat, Height ) - else - MenuText = string.format( MenuTextFormat, Height, Seconds ) - end - end - - if not self.EscortMenuHoldPosition then - self.EscortMenuHoldPosition = {} - end - - self.EscortMenuHoldPosition[#self.EscortMenuHoldPosition+1] = MENU_CLIENT_COMMAND - :New( - self.EscortClient, - MenuText, - self.EscortMenuHold, - ESCORT._HoldPosition, - self, - self.EscortGroup, - Height, - Seconds - ) - end - - return self -end - - ---- Defines a menu slot to let the escort hold at the client position and stay low with a specified height during a specified time in seconds. --- This menu will appear under **Navigation**. --- @param #ESCORT self --- @param Dcs.DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. --- @param Dcs.DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. --- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. --- @return #ESCORT --- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function. -function ESCORT:MenuHoldAtLeaderPosition( Height, Seconds, MenuTextFormat ) - self:F( { Height, Seconds, MenuTextFormat } ) - - if self.EscortGroup:IsAir() then - - if not self.EscortMenuHold then - self.EscortMenuHold = MENU_CLIENT:New( self.EscortClient, "Hold position", self.EscortMenu ) - end - - if not Height then - Height = 30 - end - - if not Seconds then - Seconds = 0 - end - - local MenuText = "" - if not MenuTextFormat then - if Seconds == 0 then - MenuText = string.format( "Rejoin and hold at %d meter", Height ) - else - MenuText = string.format( "Rejoin and hold at %d meter for %d seconds", Height, Seconds ) - end - else - if Seconds == 0 then - MenuText = string.format( MenuTextFormat, Height ) - else - MenuText = string.format( MenuTextFormat, Height, Seconds ) - end - end - - if not self.EscortMenuHoldAtLeaderPosition then - self.EscortMenuHoldAtLeaderPosition = {} - end - - self.EscortMenuHoldAtLeaderPosition[#self.EscortMenuHoldAtLeaderPosition+1] = MENU_CLIENT_COMMAND - :New( - self.EscortClient, - MenuText, - self.EscortMenuHold, - ESCORT._HoldPosition, - { ParamSelf = self, - ParamOrbitGroup = self.EscortClient, - ParamHeight = Height, - ParamSeconds = Seconds - } - ) - end - - return self -end - ---- Defines a menu slot to let the escort scan for targets at a certain height for a certain time in seconds. --- This menu will appear under **Scan targets**. --- @param #ESCORT self --- @param Dcs.DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. --- @param Dcs.DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. --- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. --- @return #ESCORT -function ESCORT:MenuScanForTargets( Height, Seconds, MenuTextFormat ) - self:F( { Height, Seconds, MenuTextFormat } ) - - if self.EscortGroup:IsAir() then - if not self.EscortMenuScan then - self.EscortMenuScan = MENU_CLIENT:New( self.EscortClient, "Scan for targets", self.EscortMenu ) - end - - if not Height then - Height = 100 - end - - if not Seconds then - Seconds = 30 - end - - local MenuText = "" - if not MenuTextFormat then - if Seconds == 0 then - MenuText = string.format( "At %d meter", Height ) - else - MenuText = string.format( "At %d meter for %d seconds", Height, Seconds ) - end - else - if Seconds == 0 then - MenuText = string.format( MenuTextFormat, Height ) - else - MenuText = string.format( MenuTextFormat, Height, Seconds ) - end - end - - if not self.EscortMenuScanForTargets then - self.EscortMenuScanForTargets = {} - end - - self.EscortMenuScanForTargets[#self.EscortMenuScanForTargets+1] = MENU_CLIENT_COMMAND - :New( - self.EscortClient, - MenuText, - self.EscortMenuScan, - ESCORT._ScanTargets, - self, - 30 - ) - end - - return self -end - - - ---- Defines a menu slot to let the escort disperse a flare in a certain color. --- This menu will appear under **Navigation**. --- The flare will be fired from the first unit in the group. --- @param #ESCORT self --- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. --- @return #ESCORT -function ESCORT:MenuFlare( MenuTextFormat ) - self:F() - - if not self.EscortMenuReportNavigation then - self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) - end - - local MenuText = "" - if not MenuTextFormat then - MenuText = "Flare" - else - MenuText = MenuTextFormat - end - - if not self.EscortMenuFlare then - self.EscortMenuFlare = MENU_CLIENT:New( self.EscortClient, MenuText, self.EscortMenuReportNavigation, ESCORT._Flare, self ) - self.EscortMenuFlareGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green flare", self.EscortMenuFlare, ESCORT._Flare, self, FLARECOLOR.Green, "Released a green flare!" ) - self.EscortMenuFlareRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red flare", self.EscortMenuFlare, ESCORT._Flare, self, FLARECOLOR.Red, "Released a red flare!" ) - self.EscortMenuFlareWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white flare", self.EscortMenuFlare, ESCORT._Flare, self, FLARECOLOR.White, "Released a white flare!" ) - self.EscortMenuFlareYellow = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release yellow flare", self.EscortMenuFlare, ESCORT._Flare, self, FLARECOLOR.Yellow, "Released a yellow flare!" ) - end - - return self -end - ---- Defines a menu slot to let the escort disperse a smoke in a certain color. --- This menu will appear under **Navigation**. --- Note that smoke menu options will only be displayed for ships and ground units. Not for air units. --- The smoke will be fired from the first unit in the group. --- @param #ESCORT self --- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. --- @return #ESCORT -function ESCORT:MenuSmoke( MenuTextFormat ) - self:F() - - if not self.EscortGroup:IsAir() then - if not self.EscortMenuReportNavigation then - self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) - end - - local MenuText = "" - if not MenuTextFormat then - MenuText = "Smoke" - else - MenuText = MenuTextFormat - end - - if not self.EscortMenuSmoke then - self.EscortMenuSmoke = MENU_CLIENT:New( self.EscortClient, "Smoke", self.EscortMenuReportNavigation, ESCORT._Smoke, self ) - self.EscortMenuSmokeGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.Green, "Releasing green smoke!" ) - self.EscortMenuSmokeRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.Red, "Releasing red smoke!" ) - self.EscortMenuSmokeWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.White, "Releasing white smoke!" ) - self.EscortMenuSmokeOrange = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release orange smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.Orange, "Releasing orange smoke!" ) - self.EscortMenuSmokeBlue = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release blue smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.Blue, "Releasing blue smoke!" ) - end - end - - return self -end - ---- Defines a menu slot to let the escort report their current detected targets with a specified time interval in seconds. --- This menu will appear under **Report targets**. --- Note that if a report targets menu is not specified, no targets will be detected by the escort, and the attack and assisted attack menus will not be displayed. --- @param #ESCORT self --- @param Dcs.DCSTypes#Time Seconds Optional parameter that lets the escort report their current detected targets after specified time interval in seconds. The default time is 30 seconds. --- @return #ESCORT -function ESCORT:MenuReportTargets( Seconds ) - self:F( { Seconds } ) - - if not self.EscortMenuReportNearbyTargets then - self.EscortMenuReportNearbyTargets = MENU_CLIENT:New( self.EscortClient, "Report targets", self.EscortMenu ) - end - - if not Seconds then - Seconds = 30 - end - - -- Report Targets - self.EscortMenuReportNearbyTargetsNow = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets now!", self.EscortMenuReportNearbyTargets, ESCORT._ReportNearbyTargetsNow, self ) - self.EscortMenuReportNearbyTargetsOn = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets on", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, self, true ) - self.EscortMenuReportNearbyTargetsOff = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets off", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, self, false ) - - -- Attack Targets - self.EscortMenuAttackNearbyTargets = MENU_CLIENT:New( self.EscortClient, "Attack targets", self.EscortMenu ) - - - self.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, {}, 1, Seconds ) - - return self -end - ---- Defines a menu slot to let the escort attack its detected targets using assisted attack from another escort joined also with the client. --- This menu will appear under **Request assistance from**. --- Note that this method needs to be preceded with the method MenuReportTargets. --- @param #ESCORT self --- @return #ESCORT -function ESCORT:MenuAssistedAttack() - self:F() - - -- Request assistance from other escorts. - -- This is very useful to let f.e. an escorting ship attack a target detected by an escorting plane... - self.EscortMenuTargetAssistance = MENU_CLIENT:New( self.EscortClient, "Request assistance from", self.EscortMenu ) - - return self -end - ---- Defines a menu to let the escort set its rules of engagement. --- All rules of engagement will appear under the menu **ROE**. --- @param #ESCORT self --- @return #ESCORT -function ESCORT:MenuROE( MenuTextFormat ) - self:F( MenuTextFormat ) - - if not self.EscortMenuROE then - -- Rules of Engagement - self.EscortMenuROE = MENU_CLIENT:New( self.EscortClient, "ROE", self.EscortMenu ) - if self.EscortGroup:OptionROEHoldFirePossible() then - self.EscortMenuROEHoldFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Hold Fire", self.EscortMenuROE, ESCORT._ROE, self, self.EscortGroup:OptionROEHoldFire(), "Holding weapons!" ) - end - if self.EscortGroup:OptionROEReturnFirePossible() then - self.EscortMenuROEReturnFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Return Fire", self.EscortMenuROE, ESCORT._ROE, self, self.EscortGroup:OptionROEReturnFire(), "Returning fire!" ) - end - if self.EscortGroup:OptionROEOpenFirePossible() then - self.EscortMenuROEOpenFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Open Fire", self.EscortMenuROE, ESCORT._ROE, self, self.EscortGroup:OptionROEOpenFire(), "Opening fire on designated targets!!" ) - end - if self.EscortGroup:OptionROEWeaponFreePossible() then - self.EscortMenuROEWeaponFree = MENU_CLIENT_COMMAND:New( self.EscortClient, "Weapon Free", self.EscortMenuROE, ESCORT._ROE, self, self.EscortGroup:OptionROEWeaponFree(), "Opening fire on targets of opportunity!" ) - end - end - - return self -end - - ---- Defines a menu to let the escort set its evasion when under threat. --- All rules of engagement will appear under the menu **Evasion**. --- @param #ESCORT self --- @return #ESCORT -function ESCORT:MenuEvasion( MenuTextFormat ) - self:F( MenuTextFormat ) - - if self.EscortGroup:IsAir() then - if not self.EscortMenuEvasion then - -- Reaction to Threats - self.EscortMenuEvasion = MENU_CLIENT:New( self.EscortClient, "Evasion", self.EscortMenu ) - if self.EscortGroup:OptionROTNoReactionPossible() then - self.EscortMenuEvasionNoReaction = MENU_CLIENT_COMMAND:New( self.EscortClient, "Fight until death", self.EscortMenuEvasion, ESCORT._ROT, self, self.EscortGroup:OptionROTNoReaction(), "Fighting until death!" ) - end - if self.EscortGroup:OptionROTPassiveDefensePossible() then - self.EscortMenuEvasionPassiveDefense = MENU_CLIENT_COMMAND:New( self.EscortClient, "Use flares, chaff and jammers", self.EscortMenuEvasion, ESCORT._ROT, self, self.EscortGroup:OptionROTPassiveDefense(), "Defending using jammers, chaff and flares!" ) - end - if self.EscortGroup:OptionROTEvadeFirePossible() then - self.EscortMenuEvasionEvadeFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Evade enemy fire", self.EscortMenuEvasion, ESCORT._ROT, self, self.EscortGroup:OptionROTEvadeFire(), "Evading on enemy fire!" ) - end - if self.EscortGroup:OptionROTVerticalPossible() then - self.EscortMenuOptionEvasionVertical = MENU_CLIENT_COMMAND:New( self.EscortClient, "Go below radar and evade fire", self.EscortMenuEvasion, ESCORT._ROT, self, self.EscortGroup:OptionROTVertical(), "Evading on enemy fire with vertical manoeuvres!" ) - end - end - end - - return self -end - ---- Defines a menu to let the escort resume its mission from a waypoint on its route. --- All rules of engagement will appear under the menu **Resume mission from**. --- @param #ESCORT self --- @return #ESCORT -function ESCORT:MenuResumeMission() - self:F() - - if not self.EscortMenuResumeMission then - -- Mission Resume Menu Root - self.EscortMenuResumeMission = MENU_CLIENT:New( self.EscortClient, "Resume mission from", self.EscortMenu ) - end - - return self -end - - ---- @param #MENUPARAM MenuParam -function ESCORT:_HoldPosition( OrbitGroup, OrbitHeight, OrbitSeconds ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local OrbitUnit = OrbitGroup:GetUnit(1) -- Wrapper.Unit#UNIT - - self.FollowScheduler:Stop( self.FollowSchedule ) - - local PointFrom = {} - local GroupVec3 = EscortGroup:GetUnit(1):GetVec3() - PointFrom = {} - PointFrom.x = GroupVec3.x - PointFrom.y = GroupVec3.z - PointFrom.speed = 250 - PointFrom.type = AI.Task.WaypointType.TURNING_POINT - PointFrom.alt = GroupVec3.y - PointFrom.alt_type = AI.Task.AltitudeType.BARO - - local OrbitPoint = OrbitUnit:GetVec2() - local PointTo = {} - PointTo.x = OrbitPoint.x - PointTo.y = OrbitPoint.y - PointTo.speed = 250 - PointTo.type = AI.Task.WaypointType.TURNING_POINT - PointTo.alt = OrbitHeight - PointTo.alt_type = AI.Task.AltitudeType.BARO - PointTo.task = EscortGroup:TaskOrbitCircleAtVec2( OrbitPoint, OrbitHeight, 0 ) - - local Points = { PointFrom, PointTo } - - EscortGroup:OptionROEHoldFire() - EscortGroup:OptionROTPassiveDefense() - - EscortGroup:SetTask( EscortGroup:TaskRoute( Points ) ) - EscortGroup:MessageToClient( "Orbiting at location.", 10, EscortClient ) - -end - ---- @param #MENUPARAM MenuParam -function ESCORT:_JoinUpAndFollow( Distance ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self.Distance = Distance - - self:JoinUpAndFollow( EscortGroup, EscortClient, self.Distance ) -end - ---- JoinsUp and Follows a CLIENT. --- @param Functional.Escort#ESCORT self --- @param Wrapper.Group#GROUP EscortGroup --- @param Wrapper.Client#CLIENT EscortClient --- @param Dcs.DCSTypes#Distance Distance -function ESCORT:JoinUpAndFollow( EscortGroup, EscortClient, Distance ) - self:F( { EscortGroup, EscortClient, Distance } ) - - self.FollowScheduler:Stop( self.FollowSchedule ) - - EscortGroup:OptionROEHoldFire() - EscortGroup:OptionROTPassiveDefense() - - self.EscortMode = ESCORT.MODE.FOLLOW - - self.CT1 = 0 - self.GT1 = 0 - self.FollowScheduler:Start( self.FollowSchedule ) - - EscortGroup:MessageToClient( "Rejoining and Following at " .. Distance .. "!", 30, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT:_Flare( Color, Message ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - EscortGroup:GetUnit(1):Flare( Color ) - EscortGroup:MessageToClient( Message, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT:_Smoke( Color, Message ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - EscortGroup:GetUnit(1):Smoke( Color ) - EscortGroup:MessageToClient( Message, 10, EscortClient ) -end - - ---- @param #MENUPARAM MenuParam -function ESCORT:_ReportNearbyTargetsNow() - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self:_ReportTargetsScheduler() - -end - -function ESCORT:_SwitchReportNearbyTargets( ReportTargets ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self.ReportTargets = ReportTargets - - if self.ReportTargets then - if not self.ReportTargetsScheduler then - self.ReportTargetsScheduler:Schedule( self, self._ReportTargetsScheduler, {}, 1, 30 ) - end - else - routines.removeFunction( self.ReportTargetsScheduler ) - self.ReportTargetsScheduler = nil - end -end - ---- @param #MENUPARAM MenuParam -function ESCORT:_ScanTargets( ScanDuration ) - - local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP - local EscortClient = self.EscortClient - - self.FollowScheduler:Stop( self.FollowSchedule ) - - if EscortGroup:IsHelicopter() then - EscortGroup:PushTask( - EscortGroup:TaskControlled( - EscortGroup:TaskOrbitCircle( 200, 20 ), - EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) - ), 1 ) - elseif EscortGroup:IsAirPlane() then - EscortGroup:PushTask( - EscortGroup:TaskControlled( - EscortGroup:TaskOrbitCircle( 1000, 500 ), - EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) - ), 1 ) - end - - EscortGroup:MessageToClient( "Scanning targets for " .. ScanDuration .. " seconds.", ScanDuration, EscortClient ) - - if self.EscortMode == ESCORT.MODE.FOLLOW then - self.FollowScheduler:Start( self.FollowSchedule ) - end - -end - ---- @param Wrapper.Group#GROUP EscortGroup -function _Resume( EscortGroup ) - env.info( '_Resume' ) - - local Escort = EscortGroup:GetState( EscortGroup, "Escort" ) - env.info( "EscortMode = " .. Escort.EscortMode ) - if Escort.EscortMode == ESCORT.MODE.FOLLOW then - Escort:JoinUpAndFollow( EscortGroup, Escort.EscortClient, Escort.Distance ) - end - -end - ---- @param #ESCORT self --- @param #number DetectedItemID -function ESCORT:_AttackTarget( DetectedItemID ) - - local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP - self:E( EscortGroup ) - - local EscortClient = self.EscortClient - - self.FollowScheduler:Stop( self.FollowSchedule ) - - if EscortGroup:IsAir() then - EscortGroup:OptionROEOpenFire() - EscortGroup:OptionROTPassiveDefense() - EscortGroup:SetState( EscortGroup, "Escort", self ) - - local DetectedSet = self.Detection:GetDetectedSet( DetectedItemID ) - - local Tasks = {} - - DetectedSet:ForEachUnit( - --- @param Wrapper.Unit#UNIT DetectedUnit - function( DetectedUnit, Tasks ) - if DetectedUnit:IsAlive() then - Tasks[#Tasks+1] = EscortGroup:TaskAttackUnit( DetectedUnit ) - end - end, Tasks - ) - - Tasks[#Tasks+1] = EscortGroup:TaskFunction( "_Resume", { "''" } ) - - EscortGroup:SetTask( - EscortGroup:TaskCombo( - Tasks - ), 1 - ) - - else - - local DetectedSet = self.Detection:GetDetectedSet( DetectedItemID ) - - local Tasks = {} - - DetectedSet:ForEachUnit( - --- @param Wrapper.Unit#UNIT DetectedUnit - function( DetectedUnit, Tasks ) - if DetectedUnit:IsAlive() then - Tasks[#Tasks+1] = EscortGroup:TaskFireAtPoint( DetectedUnit:GetVec2(), 50 ) - end - end, Tasks - ) - - EscortGroup:SetTask( - EscortGroup:TaskCombo( - Tasks - ), 1 - ) - - end - - EscortGroup:MessageToClient( "Engaging Designated Unit!", 10, EscortClient ) - -end - ---- --- @param #number DetectedItemID -function ESCORT:_AssistTarget( EscortGroupAttack, DetectedItemID ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self.FollowScheduler:Stop( self.FollowSchedule ) - - if EscortGroupAttack:IsAir() then - EscortGroupAttack:OptionROEOpenFire() - EscortGroupAttack:OptionROTVertical() - - local DetectedSet = self.Detection:GetDetectedSet( DetectedItemID ) - - local Tasks = {} - - DetectedSet:ForEachUnit( - --- @param Wrapper.Unit#UNIT DetectedUnit - function( DetectedUnit, Tasks ) - if DetectedUnit:IsAlive() then - Tasks[#Tasks+1] = EscortGroupAttack:TaskAttackUnit( DetectedUnit ) - end - end, Tasks - ) - - Tasks[#Tasks+1] = EscortGroupAttack:TaskOrbitCircle( 500, 350 ) - - EscortGroupAttack:SetTask( - EscortGroupAttack:TaskCombo( - Tasks - ), 1 - ) - - else - local DetectedSet = self.Detection:GetDetectedSet( DetectedItemID ) - - local Tasks = {} - - DetectedSet:ForEachUnit( - --- @param Wrapper.Unit#UNIT DetectedUnit - function( DetectedUnit, Tasks ) - if DetectedUnit:IsAlive() then - Tasks[#Tasks+1] = EscortGroupAttack:TaskFireAtPoint( DetectedUnit:GetVec2(), 50 ) - end - end, Tasks - ) - - EscortGroupAttack:SetTask( - EscortGroupAttack:TaskCombo( - Tasks - ), 1 - ) - - end - - EscortGroupAttack:MessageToClient( "Assisting with the destroying the enemy unit!", 10, EscortClient ) - -end - ---- @param #MENUPARAM MenuParam -function ESCORT:_ROE( EscortROEFunction, EscortROEMessage ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - pcall( function() EscortROEFunction() end ) - EscortGroup:MessageToClient( EscortROEMessage, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT:_ROT( EscortROTFunction, EscortROTMessage ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - pcall( function() EscortROTFunction() end ) - EscortGroup:MessageToClient( EscortROTMessage, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT:_ResumeMission( WayPoint ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self.FollowScheduler:Stop( self.FollowSchedule ) - - local WayPoints = EscortGroup:GetTaskRoute() - self:T( WayPoint, WayPoints ) - - for WayPointIgnore = 1, WayPoint do - table.remove( WayPoints, 1 ) - end - - SCHEDULER:New( EscortGroup, EscortGroup.SetTask, { EscortGroup:TaskRoute( WayPoints ) }, 1 ) - - EscortGroup:MessageToClient( "Resuming mission from waypoint " .. WayPoint .. ".", 10, EscortClient ) -end - ---- Registers the waypoints --- @param #ESCORT self --- @return #table -function ESCORT:RegisterRoute() - self:F() - - local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP - - local TaskPoints = EscortGroup:GetTaskRoute() - - self:T( TaskPoints ) - - return TaskPoints -end - ---- @param Functional.Escort#ESCORT self -function ESCORT:_FollowScheduler() - self:F( { self.FollowDistance } ) - - self:T( {self.EscortClient.UnitName, self.EscortGroup.GroupName } ) - if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - - local ClientUnit = self.EscortClient:GetClientGroupUnit() - local GroupUnit = self.EscortGroup:GetUnit( 1 ) - local FollowDistance = self.FollowDistance - - self:T( {ClientUnit.UnitName, GroupUnit.UnitName } ) - - if self.CT1 == 0 and self.GT1 == 0 then - self.CV1 = ClientUnit:GetVec3() - self:T( { "self.CV1", self.CV1 } ) - self.CT1 = timer.getTime() - self.GV1 = GroupUnit:GetVec3() - self.GT1 = timer.getTime() - else - local CT1 = self.CT1 - local CT2 = timer.getTime() - local CV1 = self.CV1 - local CV2 = ClientUnit:GetVec3() - self.CT1 = CT2 - self.CV1 = CV2 - - local CD = ( ( CV2.x - CV1.x )^2 + ( CV2.y - CV1.y )^2 + ( CV2.z - CV1.z )^2 ) ^ 0.5 - local CT = CT2 - CT1 - - local CS = ( 3600 / CT ) * ( CD / 1000 ) - - self:T2( { "Client:", CS, CD, CT, CV2, CV1, CT2, CT1 } ) - - local GT1 = self.GT1 - local GT2 = timer.getTime() - local GV1 = self.GV1 - local GV2 = GroupUnit:GetVec3() - self.GT1 = GT2 - self.GV1 = GV2 - - local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5 - local GT = GT2 - GT1 - - local GS = ( 3600 / GT ) * ( GD / 1000 ) - - self:T2( { "Group:", GS, GD, GT, GV2, GV1, GT2, GT1 } ) - - -- Calculate the group direction vector - local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z } - - -- Calculate GH2, GH2 with the same height as CV2. - local GH2 = { x = GV2.x, y = CV2.y, z = GV2.z } - - -- Calculate the angle of GV to the orthonormal plane - local alpha = math.atan2( GV.z, GV.x ) - - -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. - -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) - local CVI = { x = CV2.x + FollowDistance * math.cos(alpha), - y = GH2.y, - z = CV2.z + FollowDistance * math.sin(alpha), - } - - -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction. - local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z } - - -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s. - -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed. - -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right... - local DVu = { x = DV.x / FollowDistance, y = DV.y / FollowDistance, z = DV.z / FollowDistance } - - -- Now we can calculate the group destination vector GDV. - local GDV = { x = DVu.x * CS * 8 + CVI.x, y = CVI.y, z = DVu.z * CS * 8 + CVI.z } - - if self.SmokeDirectionVector == true then - trigger.action.smoke( GDV, trigger.smokeColor.Red ) - end - - self:T2( { "CV2:", CV2 } ) - self:T2( { "CVI:", CVI } ) - self:T2( { "GDV:", GDV } ) - - -- Measure distance between client and group - local CatchUpDistance = ( ( GDV.x - GV2.x )^2 + ( GDV.y - GV2.y )^2 + ( GDV.z - GV2.z )^2 ) ^ 0.5 - - -- The calculation of the Speed would simulate that the group would take 30 seconds to overcome - -- the requested Distance). - local Time = 10 - local CatchUpSpeed = ( CatchUpDistance - ( CS * 8.4 ) ) / Time - - local Speed = CS + CatchUpSpeed - if Speed < 0 then - Speed = 0 - end - - self:T( { "Client Speed, Escort Speed, Speed, FollowDistance, Time:", CS, GS, Speed, FollowDistance, Time } ) - - -- Now route the escort to the desired point with the desired speed. - self.EscortGroup:RouteToVec3( GDV, Speed / 3.6 ) -- DCS models speed in Mps (Miles per second) - end - - return true - end - - return false -end - - ---- Report Targets Scheduler. --- @param #ESCORT self -function ESCORT:_ReportTargetsScheduler() - self:F( self.EscortGroup:GetName() ) - - if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - - if true then - - local EscortGroupName = self.EscortGroup:GetName() - - self.EscortMenuAttackNearbyTargets:RemoveSubMenus() - - if self.EscortMenuTargetAssistance then - self.EscortMenuTargetAssistance:RemoveSubMenus() - end - - local DetectedItems = self.Detection:GetDetectedItems() - self:E( DetectedItems ) - - local DetectedTargets = false - - local DetectedMsgs = {} - - for ClientEscortGroupName, EscortGroupData in pairs( self.EscortClient._EscortGroups ) do - - local ClientEscortTargets = EscortGroupData.Detection - --local EscortUnit = EscortGroupData:GetUnit( 1 ) - - for DetectedItemID, DetectedItem in pairs( DetectedItems ) do - self:E( { DetectedItemID, DetectedItem } ) - -- Remove the sub menus of the Attack menu of the Escort for the EscortGroup. - - local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItemID, EscortGroupData.EscortGroup, _DATABASE:GetPlayerSettings( self.EscortClient:GetPlayerName() ) ) - - if ClientEscortGroupName == EscortGroupName then - - local DetectedMsg = DetectedItemReportSummary:Text("\n") - DetectedMsgs[#DetectedMsgs+1] = DetectedMsg - - self:T( DetectedMsg ) - - MENU_CLIENT_COMMAND:New( self.EscortClient, - DetectedMsg, - self.EscortMenuAttackNearbyTargets, - ESCORT._AttackTarget, - self, - DetectedItemID - ) - else - if self.EscortMenuTargetAssistance then - - local DetectedMsg = DetectedItemReportSummary:Text("\n") - self:T( DetectedMsg ) - - local MenuTargetAssistance = MENU_CLIENT:New( self.EscortClient, EscortGroupData.EscortName, self.EscortMenuTargetAssistance ) - MENU_CLIENT_COMMAND:New( self.EscortClient, - DetectedMsg, - MenuTargetAssistance, - ESCORT._AssistTarget, - self, - EscortGroupData.EscortGroup, - DetectedItemID - ) - end - end - - DetectedTargets = true - - end - end - self:E( DetectedMsgs ) - if DetectedTargets then - self.EscortGroup:MessageToClient( "Reporting detected targets:\n" .. table.concat( DetectedMsgs, "\n" ), 20, self.EscortClient ) - else - self.EscortGroup:MessageToClient( "No targets detected.", 10, self.EscortClient ) - end - - return true - else --- local EscortGroupName = self.EscortGroup:GetName() --- local EscortTargets = self.EscortGroup:GetDetectedTargets() --- --- local ClientEscortTargets = self.EscortClient._EscortGroups[EscortGroupName].Targets --- --- local EscortTargetMessages = "" --- for EscortTargetID, EscortTarget in pairs( EscortTargets ) do --- local EscortObject = EscortTarget.object --- self:T( EscortObject ) --- if EscortObject and EscortObject:isExist() and EscortObject.id_ < 50000000 then --- --- local EscortTargetUnit = UNIT:Find( EscortObject ) --- local EscortTargetUnitName = EscortTargetUnit:GetName() --- --- --- --- -- local EscortTargetIsDetected, --- -- EscortTargetIsVisible, --- -- EscortTargetLastTime, --- -- EscortTargetKnowType, --- -- EscortTargetKnowDistance, --- -- EscortTargetLastPos, --- -- EscortTargetLastVelocity --- -- = self.EscortGroup:IsTargetDetected( EscortObject ) --- -- --- -- self:T( { EscortTargetIsDetected, --- -- EscortTargetIsVisible, --- -- EscortTargetLastTime, --- -- EscortTargetKnowType, --- -- EscortTargetKnowDistance, --- -- EscortTargetLastPos, --- -- EscortTargetLastVelocity } ) --- --- --- local EscortTargetUnitVec3 = EscortTargetUnit:GetVec3() --- local EscortVec3 = self.EscortGroup:GetVec3() --- local Distance = ( ( EscortTargetUnitVec3.x - EscortVec3.x )^2 + --- ( EscortTargetUnitVec3.y - EscortVec3.y )^2 + --- ( EscortTargetUnitVec3.z - EscortVec3.z )^2 --- ) ^ 0.5 / 1000 --- --- self:T( { self.EscortGroup:GetName(), EscortTargetUnit:GetName(), Distance, EscortTarget } ) --- --- if Distance <= 15 then --- --- if not ClientEscortTargets[EscortTargetUnitName] then --- ClientEscortTargets[EscortTargetUnitName] = {} --- end --- ClientEscortTargets[EscortTargetUnitName].AttackUnit = EscortTargetUnit --- ClientEscortTargets[EscortTargetUnitName].visible = EscortTarget.visible --- ClientEscortTargets[EscortTargetUnitName].type = EscortTarget.type --- ClientEscortTargets[EscortTargetUnitName].distance = EscortTarget.distance --- else --- if ClientEscortTargets[EscortTargetUnitName] then --- ClientEscortTargets[EscortTargetUnitName] = nil --- end --- end --- end --- end --- --- self:T( { "Sorting Targets Table:", ClientEscortTargets } ) --- table.sort( ClientEscortTargets, function( a, b ) return a.Distance < b.Distance end ) --- self:T( { "Sorted Targets Table:", ClientEscortTargets } ) --- --- -- Remove the sub menus of the Attack menu of the Escort for the EscortGroup. --- self.EscortMenuAttackNearbyTargets:RemoveSubMenus() --- --- if self.EscortMenuTargetAssistance then --- self.EscortMenuTargetAssistance:RemoveSubMenus() --- end --- --- --for MenuIndex = 1, #self.EscortMenuAttackTargets do --- -- self:T( { "Remove Menu:", self.EscortMenuAttackTargets[MenuIndex] } ) --- -- self.EscortMenuAttackTargets[MenuIndex] = self.EscortMenuAttackTargets[MenuIndex]:Remove() --- --end --- --- --- if ClientEscortTargets then --- for ClientEscortTargetUnitName, ClientEscortTargetData in pairs( ClientEscortTargets ) do --- --- for ClientEscortGroupName, EscortGroupData in pairs( self.EscortClient._EscortGroups ) do --- --- if ClientEscortTargetData and ClientEscortTargetData.AttackUnit:IsAlive() then --- --- local EscortTargetMessage = "" --- local EscortTargetCategoryName = ClientEscortTargetData.AttackUnit:GetCategoryName() --- local EscortTargetCategoryType = ClientEscortTargetData.AttackUnit:GetTypeName() --- if ClientEscortTargetData.type then --- EscortTargetMessage = EscortTargetMessage .. EscortTargetCategoryName .. " (" .. EscortTargetCategoryType .. ") at " --- else --- EscortTargetMessage = EscortTargetMessage .. "Unknown target at " --- end --- --- local EscortTargetUnitVec3 = ClientEscortTargetData.AttackUnit:GetVec3() --- local EscortVec3 = self.EscortGroup:GetVec3() --- local Distance = ( ( EscortTargetUnitVec3.x - EscortVec3.x )^2 + --- ( EscortTargetUnitVec3.y - EscortVec3.y )^2 + --- ( EscortTargetUnitVec3.z - EscortVec3.z )^2 --- ) ^ 0.5 / 1000 --- --- self:T( { self.EscortGroup:GetName(), ClientEscortTargetData.AttackUnit:GetName(), Distance, ClientEscortTargetData.AttackUnit } ) --- if ClientEscortTargetData.visible == false then --- EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " estimated km" --- else --- EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " km" --- end --- --- if ClientEscortTargetData.visible then --- EscortTargetMessage = EscortTargetMessage .. ", visual" --- end --- --- if ClientEscortGroupName == EscortGroupName then --- --- MENU_CLIENT_COMMAND:New( self.EscortClient, --- EscortTargetMessage, --- self.EscortMenuAttackNearbyTargets, --- ESCORT._AttackTarget, --- { ParamSelf = self, --- ParamUnit = ClientEscortTargetData.AttackUnit --- } --- ) --- EscortTargetMessages = EscortTargetMessages .. "\n - " .. EscortTargetMessage --- else --- if self.EscortMenuTargetAssistance then --- local MenuTargetAssistance = MENU_CLIENT:New( self.EscortClient, EscortGroupData.EscortName, self.EscortMenuTargetAssistance ) --- MENU_CLIENT_COMMAND:New( self.EscortClient, --- EscortTargetMessage, --- MenuTargetAssistance, --- ESCORT._AssistTarget, --- self, --- EscortGroupData.EscortGroup, --- ClientEscortTargetData.AttackUnit --- ) --- end --- end --- else --- ClientEscortTargetData = nil --- end --- end --- end --- --- if EscortTargetMessages ~= "" and self.ReportTargets == true then --- self.EscortGroup:MessageToClient( "Detected targets within 15 km range:" .. EscortTargetMessages:gsub("\n$",""), 20, self.EscortClient ) --- else --- self.EscortGroup:MessageToClient( "No targets detected!", 20, self.EscortClient ) --- end --- end --- --- if self.EscortMenuResumeMission then --- self.EscortMenuResumeMission:RemoveSubMenus() --- --- -- if self.EscortMenuResumeWayPoints then --- -- for MenuIndex = 1, #self.EscortMenuResumeWayPoints do --- -- self:T( { "Remove Menu:", self.EscortMenuResumeWayPoints[MenuIndex] } ) --- -- self.EscortMenuResumeWayPoints[MenuIndex] = self.EscortMenuResumeWayPoints[MenuIndex]:Remove() --- -- end --- -- end --- --- local TaskPoints = self:RegisterRoute() --- for WayPointID, WayPoint in pairs( TaskPoints ) do --- local EscortVec3 = self.EscortGroup:GetVec3() --- local Distance = ( ( WayPoint.x - EscortVec3.x )^2 + --- ( WayPoint.y - EscortVec3.z )^2 --- ) ^ 0.5 / 1000 --- MENU_CLIENT_COMMAND:New( self.EscortClient, "Waypoint " .. WayPointID .. " at " .. string.format( "%.2f", Distance ).. "km", self.EscortMenuResumeMission, ESCORT._ResumeMission, { ParamSelf = self, ParamWayPoint = WayPointID } ) --- end --- end --- --- return true - end - end - - return false -end ---- **Functional** -- MISSILETRAINER helps you to train missile avoidance. --- --- === --- --- 1) @{MissileTrainer#MISSILETRAINER} class, extends @{Base#BASE} --- =============================================================== --- The @{#MISSILETRAINER} class uses the DCS world messaging system to be alerted of any missiles fired, and when a missile would hit your aircraft, --- the class will destroy the missile within a certain range, to avoid damage to your aircraft. --- It suports the following functionality: --- --- * Track the missiles fired at you and other players, providing bearing and range information of the missiles towards the airplanes. --- * Provide alerts of missile launches, including detailed information of the units launching, including bearing, range � --- * Provide alerts when a missile would have killed your aircraft. --- * Provide alerts when the missile self destructs. --- * Enable / Disable and Configure the Missile Trainer using the various menu options. --- --- When running a mission where MISSILETRAINER is used, the following radio menu structure ( 'Radio Menu' -> 'Other (F10)' -> 'MissileTrainer' ) options are available for the players: --- --- * **Messages**: Menu to configure all messages. --- * **Messages On**: Show all messages. --- * **Messages Off**: Disable all messages. --- * **Tracking**: Menu to configure missile tracking messages. --- * **To All**: Shows missile tracking messages to all players. --- * **To Target**: Shows missile tracking messages only to the player where the missile is targetted at. --- * **Tracking On**: Show missile tracking messages. --- * **Tracking Off**: Disable missile tracking messages. --- * **Frequency Increase**: Increases the missile tracking message frequency with one second. --- * **Frequency Decrease**: Decreases the missile tracking message frequency with one second. --- * **Alerts**: Menu to configure alert messages. --- * **To All**: Shows alert messages to all players. --- * **To Target**: Shows alert messages only to the player where the missile is (was) targetted at. --- * **Hits On**: Show missile hit alert messages. --- * **Hits Off**: Disable missile hit alert messages. --- * **Launches On**: Show missile launch messages. --- * **Launches Off**: Disable missile launch messages. --- * **Details**: Menu to configure message details. --- * **Range On**: Shows range information when a missile is fired to a target. --- * **Range Off**: Disable range information when a missile is fired to a target. --- * **Bearing On**: Shows bearing information when a missile is fired to a target. --- * **Bearing Off**: Disable bearing information when a missile is fired to a target. --- * **Distance**: Menu to configure the distance when a missile needs to be destroyed when near to a player, during tracking. This will improve/influence hit calculation accuracy, but has the risk of damaging the aircraft when the missile reaches the aircraft before the distance is measured. --- * **50 meter**: Destroys the missile when the distance to the aircraft is below or equal to 50 meter. --- * **100 meter**: Destroys the missile when the distance to the aircraft is below or equal to 100 meter. --- * **150 meter**: Destroys the missile when the distance to the aircraft is below or equal to 150 meter. --- * **200 meter**: Destroys the missile when the distance to the aircraft is below or equal to 200 meter. --- --- --- 1.1) MISSILETRAINER construction methods: --- ----------------------------------------- --- Create a new MISSILETRAINER object with the @{#MISSILETRAINER.New} method: --- --- * @{#MISSILETRAINER.New}: Creates a new MISSILETRAINER object taking the maximum distance to your aircraft to evaluate when a missile needs to be destroyed. --- --- MISSILETRAINER will collect each unit declared in the mission with a skill level "Client" and "Player", and will monitor the missiles shot at those. --- --- 1.2) MISSILETRAINER initialization methods: --- ------------------------------------------- --- A MISSILETRAINER object will behave differently based on the usage of initialization methods: --- --- * @{#MISSILETRAINER.InitMessagesOnOff}: Sets by default the display of any message to be ON or OFF. --- * @{#MISSILETRAINER.InitTrackingToAll}: Sets by default the missile tracking report for all players or only for those missiles targetted to you. --- * @{#MISSILETRAINER.InitTrackingOnOff}: Sets by default the display of missile tracking report to be ON or OFF. --- * @{#MISSILETRAINER.InitTrackingFrequency}: Increases, decreases the missile tracking message display frequency with the provided time interval in seconds. --- * @{#MISSILETRAINER.InitAlertsToAll}: Sets by default the display of alerts to be shown to all players or only to you. --- * @{#MISSILETRAINER.InitAlertsHitsOnOff}: Sets by default the display of hit alerts ON or OFF. --- * @{#MISSILETRAINER.InitAlertsLaunchesOnOff}: Sets by default the display of launch alerts ON or OFF. --- * @{#MISSILETRAINER.InitRangeOnOff}: Sets by default the display of range information of missiles ON of OFF. --- * @{#MISSILETRAINER.InitBearingOnOff}: Sets by default the display of bearing information of missiles ON of OFF. --- * @{#MISSILETRAINER.InitMenusOnOff}: Allows to configure the options through the radio menu. --- --- === --- --- CREDITS --- ======= --- **Stuka (Danny)** Who you can search on the Eagle Dynamics Forums. --- Working together with Danny has resulted in the MISSILETRAINER class. --- Danny has shared his ideas and together we made a design. --- Together with the **476 virtual team**, we tested the MISSILETRAINER class, and got much positive feedback! --- --- @module MissileTrainer --- @author FlightControl - - ---- The MISSILETRAINER class --- @type MISSILETRAINER --- @field Core.Set#SET_CLIENT DBClients --- @extends Core.Base#BASE -MISSILETRAINER = { - ClassName = "MISSILETRAINER", - TrackingMissiles = {}, -} - -function MISSILETRAINER._Alive( Client, self ) - - if self.Briefing then - Client:Message( self.Briefing, 15, "Trainer" ) - end - - if self.MenusOnOff == true then - Client:Message( "Use the 'Radio Menu' -> 'Other (F10)' -> 'Missile Trainer' menu options to change the Missile Trainer settings (for all players).", 15, "Trainer" ) - - Client.MainMenu = MENU_CLIENT:New( Client, "Missile Trainer", nil ) -- Menu#MENU_CLIENT - - Client.MenuMessages = MENU_CLIENT:New( Client, "Messages", Client.MainMenu ) - Client.MenuOn = MENU_CLIENT_COMMAND:New( Client, "Messages On", Client.MenuMessages, self._MenuMessages, { MenuSelf = self, MessagesOnOff = true } ) - Client.MenuOff = MENU_CLIENT_COMMAND:New( Client, "Messages Off", Client.MenuMessages, self._MenuMessages, { MenuSelf = self, MessagesOnOff = false } ) - - Client.MenuTracking = MENU_CLIENT:New( Client, "Tracking", Client.MainMenu ) - Client.MenuTrackingToAll = MENU_CLIENT_COMMAND:New( Client, "To All", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingToAll = true } ) - Client.MenuTrackingToTarget = MENU_CLIENT_COMMAND:New( Client, "To Target", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingToAll = false } ) - Client.MenuTrackOn = MENU_CLIENT_COMMAND:New( Client, "Tracking On", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingOnOff = true } ) - Client.MenuTrackOff = MENU_CLIENT_COMMAND:New( Client, "Tracking Off", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingOnOff = false } ) - Client.MenuTrackIncrease = MENU_CLIENT_COMMAND:New( Client, "Frequency Increase", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingFrequency = -1 } ) - Client.MenuTrackDecrease = MENU_CLIENT_COMMAND:New( Client, "Frequency Decrease", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingFrequency = 1 } ) - - Client.MenuAlerts = MENU_CLIENT:New( Client, "Alerts", Client.MainMenu ) - Client.MenuAlertsToAll = MENU_CLIENT_COMMAND:New( Client, "To All", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsToAll = true } ) - Client.MenuAlertsToTarget = MENU_CLIENT_COMMAND:New( Client, "To Target", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsToAll = false } ) - Client.MenuHitsOn = MENU_CLIENT_COMMAND:New( Client, "Hits On", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsHitsOnOff = true } ) - Client.MenuHitsOff = MENU_CLIENT_COMMAND:New( Client, "Hits Off", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsHitsOnOff = false } ) - Client.MenuLaunchesOn = MENU_CLIENT_COMMAND:New( Client, "Launches On", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsLaunchesOnOff = true } ) - Client.MenuLaunchesOff = MENU_CLIENT_COMMAND:New( Client, "Launches Off", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsLaunchesOnOff = false } ) - - Client.MenuDetails = MENU_CLIENT:New( Client, "Details", Client.MainMenu ) - Client.MenuDetailsDistanceOn = MENU_CLIENT_COMMAND:New( Client, "Range On", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsRangeOnOff = true } ) - Client.MenuDetailsDistanceOff = MENU_CLIENT_COMMAND:New( Client, "Range Off", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsRangeOnOff = false } ) - Client.MenuDetailsBearingOn = MENU_CLIENT_COMMAND:New( Client, "Bearing On", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsBearingOnOff = true } ) - Client.MenuDetailsBearingOff = MENU_CLIENT_COMMAND:New( Client, "Bearing Off", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsBearingOnOff = false } ) - - Client.MenuDistance = MENU_CLIENT:New( Client, "Set distance to plane", Client.MainMenu ) - Client.MenuDistance50 = MENU_CLIENT_COMMAND:New( Client, "50 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 50 / 1000 } ) - Client.MenuDistance100 = MENU_CLIENT_COMMAND:New( Client, "100 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 100 / 1000 } ) - Client.MenuDistance150 = MENU_CLIENT_COMMAND:New( Client, "150 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 150 / 1000 } ) - Client.MenuDistance200 = MENU_CLIENT_COMMAND:New( Client, "200 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 200 / 1000 } ) - else - if Client.MainMenu then - Client.MainMenu:Remove() - end - end - - local ClientID = Client:GetID() - self:T( ClientID ) - if not self.TrackingMissiles[ClientID] then - self.TrackingMissiles[ClientID] = {} - end - self.TrackingMissiles[ClientID].Client = Client - if not self.TrackingMissiles[ClientID].MissileData then - self.TrackingMissiles[ClientID].MissileData = {} - end -end - ---- Creates the main object which is handling missile tracking. --- When a missile is fired a SCHEDULER is set off that follows the missile. When near a certain a client player, the missile will be destroyed. --- @param #MISSILETRAINER self --- @param #number Distance The distance in meters when a tracked missile needs to be destroyed when close to a player. --- @param #string Briefing (Optional) Will show a text to the players when starting their mission. Can be used for briefing purposes. --- @return #MISSILETRAINER -function MISSILETRAINER:New( Distance, Briefing ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( Distance ) - - if Briefing then - self.Briefing = Briefing - end - - self.Schedulers = {} - self.SchedulerID = 0 - - self.MessageInterval = 2 - self.MessageLastTime = timer.getTime() - - self.Distance = Distance / 1000 - - self:HandleEvent( EVENTS.Shot ) - - self.DBClients = SET_CLIENT:New():FilterStart() - - --- for ClientID, Client in pairs( self.DBClients.Database ) do --- self:E( "ForEach:" .. Client.UnitName ) --- Client:Alive( self._Alive, self ) --- end --- - self.DBClients:ForEachClient( - function( Client ) - self:E( "ForEach:" .. Client.UnitName ) - Client:Alive( self._Alive, self ) - end - ) - - - --- self.DB:ForEachClient( --- --- @param Wrapper.Client#CLIENT Client --- function( Client ) --- --- ... actions ... --- --- end --- ) - - self.MessagesOnOff = true - - self.TrackingToAll = false - self.TrackingOnOff = true - self.TrackingFrequency = 3 - - self.AlertsToAll = true - self.AlertsHitsOnOff = true - self.AlertsLaunchesOnOff = true - - self.DetailsRangeOnOff = true - self.DetailsBearingOnOff = true - - self.MenusOnOff = true - - self.TrackingMissiles = {} - - self.TrackingScheduler = SCHEDULER:New( self, self._TrackMissiles, {}, 0.5, 0.05, 0 ) - - return self -end - --- Initialization methods. - - - ---- Sets by default the display of any message to be ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean MessagesOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitMessagesOnOff( MessagesOnOff ) - self:F( MessagesOnOff ) - - self.MessagesOnOff = MessagesOnOff - if self.MessagesOnOff == true then - MESSAGE:New( "Messages ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Messages OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the missile tracking report for all players or only for those missiles targetted to you. --- @param #MISSILETRAINER self --- @param #boolean TrackingToAll true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitTrackingToAll( TrackingToAll ) - self:F( TrackingToAll ) - - self.TrackingToAll = TrackingToAll - if self.TrackingToAll == true then - MESSAGE:New( "Missile tracking to all players ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Missile tracking to all players OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of missile tracking report to be ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean TrackingOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitTrackingOnOff( TrackingOnOff ) - self:F( TrackingOnOff ) - - self.TrackingOnOff = TrackingOnOff - if self.TrackingOnOff == true then - MESSAGE:New( "Missile tracking ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Missile tracking OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Increases, decreases the missile tracking message display frequency with the provided time interval in seconds. --- The default frequency is a 3 second interval, so the Tracking Frequency parameter specifies the increase or decrease from the default 3 seconds or the last frequency update. --- @param #MISSILETRAINER self --- @param #number TrackingFrequency Provide a negative or positive value in seconds to incraese or decrease the display frequency. --- @return #MISSILETRAINER self -function MISSILETRAINER:InitTrackingFrequency( TrackingFrequency ) - self:F( TrackingFrequency ) - - self.TrackingFrequency = self.TrackingFrequency + TrackingFrequency - if self.TrackingFrequency < 0.5 then - self.TrackingFrequency = 0.5 - end - if self.TrackingFrequency then - MESSAGE:New( "Missile tracking frequency is " .. self.TrackingFrequency .. " seconds.", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of alerts to be shown to all players or only to you. --- @param #MISSILETRAINER self --- @param #boolean AlertsToAll true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitAlertsToAll( AlertsToAll ) - self:F( AlertsToAll ) - - self.AlertsToAll = AlertsToAll - if self.AlertsToAll == true then - MESSAGE:New( "Alerts to all players ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Alerts to all players OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of hit alerts ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean AlertsHitsOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitAlertsHitsOnOff( AlertsHitsOnOff ) - self:F( AlertsHitsOnOff ) - - self.AlertsHitsOnOff = AlertsHitsOnOff - if self.AlertsHitsOnOff == true then - MESSAGE:New( "Alerts Hits ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Alerts Hits OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of launch alerts ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean AlertsLaunchesOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitAlertsLaunchesOnOff( AlertsLaunchesOnOff ) - self:F( AlertsLaunchesOnOff ) - - self.AlertsLaunchesOnOff = AlertsLaunchesOnOff - if self.AlertsLaunchesOnOff == true then - MESSAGE:New( "Alerts Launches ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Alerts Launches OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of range information of missiles ON of OFF. --- @param #MISSILETRAINER self --- @param #boolean DetailsRangeOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitRangeOnOff( DetailsRangeOnOff ) - self:F( DetailsRangeOnOff ) - - self.DetailsRangeOnOff = DetailsRangeOnOff - if self.DetailsRangeOnOff == true then - MESSAGE:New( "Range display ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Range display OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of bearing information of missiles ON of OFF. --- @param #MISSILETRAINER self --- @param #boolean DetailsBearingOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitBearingOnOff( DetailsBearingOnOff ) - self:F( DetailsBearingOnOff ) - - self.DetailsBearingOnOff = DetailsBearingOnOff - if self.DetailsBearingOnOff == true then - MESSAGE:New( "Bearing display OFF", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Bearing display OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Enables / Disables the menus. --- @param #MISSILETRAINER self --- @param #boolean MenusOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitMenusOnOff( MenusOnOff ) - self:F( MenusOnOff ) - - self.MenusOnOff = MenusOnOff - if self.MenusOnOff == true then - MESSAGE:New( "Menus are ENABLED (only when a player rejoins a slot)", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Menus are DISABLED", 15, "Menu" ):ToAll() - end - - return self -end - - --- Menu functions - -function MISSILETRAINER._MenuMessages( MenuParameters ) - - local self = MenuParameters.MenuSelf - - if MenuParameters.MessagesOnOff ~= nil then - self:InitMessagesOnOff( MenuParameters.MessagesOnOff ) - end - - if MenuParameters.TrackingToAll ~= nil then - self:InitTrackingToAll( MenuParameters.TrackingToAll ) - end - - if MenuParameters.TrackingOnOff ~= nil then - self:InitTrackingOnOff( MenuParameters.TrackingOnOff ) - end - - if MenuParameters.TrackingFrequency ~= nil then - self:InitTrackingFrequency( MenuParameters.TrackingFrequency ) - end - - if MenuParameters.AlertsToAll ~= nil then - self:InitAlertsToAll( MenuParameters.AlertsToAll ) - end - - if MenuParameters.AlertsHitsOnOff ~= nil then - self:InitAlertsHitsOnOff( MenuParameters.AlertsHitsOnOff ) - end - - if MenuParameters.AlertsLaunchesOnOff ~= nil then - self:InitAlertsLaunchesOnOff( MenuParameters.AlertsLaunchesOnOff ) - end - - if MenuParameters.DetailsRangeOnOff ~= nil then - self:InitRangeOnOff( MenuParameters.DetailsRangeOnOff ) - end - - if MenuParameters.DetailsBearingOnOff ~= nil then - self:InitBearingOnOff( MenuParameters.DetailsBearingOnOff ) - end - - if MenuParameters.Distance ~= nil then - self.Distance = MenuParameters.Distance - MESSAGE:New( "Hit detection distance set to " .. ( self.Distance * 1000 ) .. " meters", 15, "Menu" ):ToAll() - end - -end - ---- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. --- @param #MISSILETRAINER self --- @param Core.Event#EVENTDATA EventData -function MISSILETRAINER:OnEventShot( EVentData ) - self:F( { EVentData } ) - - local TrainerSourceDCSUnit = EVentData.IniDCSUnit - local TrainerSourceDCSUnitName = EVentData.IniDCSUnitName - local TrainerWeapon = EVentData.Weapon -- Identify the weapon fired - local TrainerWeaponName = EVentData.WeaponName -- return weapon type - - self:T( "Missile Launched = " .. TrainerWeaponName ) - - local TrainerTargetDCSUnit = TrainerWeapon:getTarget() -- Identify target - if TrainerTargetDCSUnit then - local TrainerTargetDCSUnitName = Unit.getName( TrainerTargetDCSUnit ) - local TrainerTargetSkill = _DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill - - self:T(TrainerTargetDCSUnitName ) - - local Client = self.DBClients:FindClient( TrainerTargetDCSUnitName ) - if Client then - - local TrainerSourceUnit = UNIT:Find( TrainerSourceDCSUnit ) - local TrainerTargetUnit = UNIT:Find( TrainerTargetDCSUnit ) - - if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then - - local Message = MESSAGE:New( - string.format( "%s launched a %s", - TrainerSourceUnit:GetTypeName(), - TrainerWeaponName - ) .. self:_AddRange( Client, TrainerWeapon ) .. self:_AddBearing( Client, TrainerWeapon ), 5, "Launch Alert" ) - - if self.AlertsToAll then - Message:ToAll() - else - Message:ToClient( Client ) - end - end - - local ClientID = Client:GetID() - self:T( ClientID ) - local MissileData = {} - MissileData.TrainerSourceUnit = TrainerSourceUnit - MissileData.TrainerWeapon = TrainerWeapon - MissileData.TrainerTargetUnit = TrainerTargetUnit - MissileData.TrainerWeaponTypeName = TrainerWeapon:getTypeName() - MissileData.TrainerWeaponLaunched = true - table.insert( self.TrackingMissiles[ClientID].MissileData, MissileData ) - --self:T( self.TrackingMissiles ) - end - else - -- TODO: some weapons don't know the target unit... Need to develop a workaround for this. - if ( TrainerWeapon:getTypeName() == "9M311" ) then - SCHEDULER:New( TrainerWeapon, TrainerWeapon.destroy, {}, 1 ) - else - end - end -end - -function MISSILETRAINER:_AddRange( Client, TrainerWeapon ) - - local RangeText = "" - - if self.DetailsRangeOnOff then - - local PositionMissile = TrainerWeapon:getPoint() - local TargetVec3 = Client:GetVec3() - - local Range = ( ( PositionMissile.x - TargetVec3.x )^2 + - ( PositionMissile.y - TargetVec3.y )^2 + - ( PositionMissile.z - TargetVec3.z )^2 - ) ^ 0.5 / 1000 - - RangeText = string.format( ", at %4.2fkm", Range ) - end - - return RangeText -end - -function MISSILETRAINER:_AddBearing( Client, TrainerWeapon ) - - local BearingText = "" - - if self.DetailsBearingOnOff then - - local PositionMissile = TrainerWeapon:getPoint() - local TargetVec3 = Client:GetVec3() - - self:T2( { TargetVec3, PositionMissile }) - - local DirectionVector = { x = PositionMissile.x - TargetVec3.x, y = PositionMissile.y - TargetVec3.y, z = PositionMissile.z - TargetVec3.z } - local DirectionRadians = math.atan2( DirectionVector.z, DirectionVector.x ) - --DirectionRadians = DirectionRadians + routines.getNorthCorrection( PositionTarget ) - if DirectionRadians < 0 then - DirectionRadians = DirectionRadians + 2 * math.pi - end - local DirectionDegrees = DirectionRadians * 180 / math.pi - - BearingText = string.format( ", %d degrees", DirectionDegrees ) - end - - return BearingText -end - - -function MISSILETRAINER:_TrackMissiles() - self:F2() - - - local ShowMessages = false - if self.MessagesOnOff and self.MessageLastTime + self.TrackingFrequency <= timer.getTime() then - self.MessageLastTime = timer.getTime() - ShowMessages = true - end - - -- ALERTS PART - - -- Loop for all Player Clients to check the alerts and deletion of missiles. - for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do - - local Client = ClientData.Client - - if Client and Client:IsAlive() then - - for MissileDataID, MissileData in pairs( ClientData.MissileData ) do - self:T3( MissileDataID ) - - local TrainerSourceUnit = MissileData.TrainerSourceUnit - local TrainerWeapon = MissileData.TrainerWeapon - local TrainerTargetUnit = MissileData.TrainerTargetUnit - local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName - local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched - - if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then - local PositionMissile = TrainerWeapon:getPosition().p - local TargetVec3 = Client:GetVec3() - - local Distance = ( ( PositionMissile.x - TargetVec3.x )^2 + - ( PositionMissile.y - TargetVec3.y )^2 + - ( PositionMissile.z - TargetVec3.z )^2 - ) ^ 0.5 / 1000 - - if Distance <= self.Distance then - -- Hit alert - TrainerWeapon:destroy() - if self.MessagesOnOff == true and self.AlertsHitsOnOff == true then - - self:T( "killed" ) - - local Message = MESSAGE:New( - string.format( "%s launched by %s killed %s", - TrainerWeapon:getTypeName(), - TrainerSourceUnit:GetTypeName(), - TrainerTargetUnit:GetPlayerName() - ), 15, "Hit Alert" ) - - if self.AlertsToAll == true then - Message:ToAll() - else - Message:ToClient( Client ) - end - - MissileData = nil - table.remove( ClientData.MissileData, MissileDataID ) - self:T(ClientData.MissileData) - end - end - else - if not ( TrainerWeapon and TrainerWeapon:isExist() ) then - if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then - -- Weapon does not exist anymore. Delete from Table - local Message = MESSAGE:New( - string.format( "%s launched by %s self destructed!", - TrainerWeaponTypeName, - TrainerSourceUnit:GetTypeName() - ), 5, "Tracking" ) - - if self.AlertsToAll == true then - Message:ToAll() - else - Message:ToClient( Client ) - end - end - MissileData = nil - table.remove( ClientData.MissileData, MissileDataID ) - self:T( ClientData.MissileData ) - end - end - end - else - self.TrackingMissiles[ClientDataID] = nil - end - end - - if ShowMessages == true and self.MessagesOnOff == true and self.TrackingOnOff == true then -- Only do this when tracking information needs to be displayed. - - -- TRACKING PART - - -- For the current client, the missile range and bearing details are displayed To the Player Client. - -- For the other clients, the missile range and bearing details are displayed To the other Player Clients. - -- To achieve this, a cross loop is done for each Player Client <-> Other Player Client missile information. - - -- Main Player Client loop - for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do - - local Client = ClientData.Client - --self:T2( { Client:GetName() } ) - - - ClientData.MessageToClient = "" - ClientData.MessageToAll = "" - - -- Other Players Client loop - for TrackingDataID, TrackingData in pairs( self.TrackingMissiles ) do - - for MissileDataID, MissileData in pairs( TrackingData.MissileData ) do - --self:T3( MissileDataID ) - - local TrainerSourceUnit = MissileData.TrainerSourceUnit - local TrainerWeapon = MissileData.TrainerWeapon - local TrainerTargetUnit = MissileData.TrainerTargetUnit - local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName - local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched - - if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then - - if ShowMessages == true then - local TrackingTo - TrackingTo = string.format( " -> %s", - TrainerWeaponTypeName - ) - - if ClientDataID == TrackingDataID then - if ClientData.MessageToClient == "" then - ClientData.MessageToClient = "Missiles to You:\n" - end - ClientData.MessageToClient = ClientData.MessageToClient .. TrackingTo .. self:_AddRange( ClientData.Client, TrainerWeapon ) .. self:_AddBearing( ClientData.Client, TrainerWeapon ) .. "\n" - else - if self.TrackingToAll == true then - if ClientData.MessageToAll == "" then - ClientData.MessageToAll = "Missiles to other Players:\n" - end - ClientData.MessageToAll = ClientData.MessageToAll .. TrackingTo .. self:_AddRange( ClientData.Client, TrainerWeapon ) .. self:_AddBearing( ClientData.Client, TrainerWeapon ) .. " ( " .. TrainerTargetUnit:GetPlayerName() .. " )\n" - end - end - end - end - end - end - - -- Once the Player Client and the Other Player Client tracking messages are prepared, show them. - if ClientData.MessageToClient ~= "" or ClientData.MessageToAll ~= "" then - local Message = MESSAGE:New( ClientData.MessageToClient .. ClientData.MessageToAll, 1, "Tracking" ):ToClient( Client ) - end - end - end - - return true -end ---- **Functional** -- This module monitors airbases traffic. --- --- === --- --- 1) @{AirbasePolice#AIRBASEPOLICE_BASE} class, extends @{Base#BASE} --- ================================================================== --- The @{AirbasePolice#AIRBASEPOLICE_BASE} class provides the main methods to monitor CLIENT behaviour at airbases. --- CLIENTS should not be allowed to: --- --- * Don't taxi faster than 40 km/h. --- * Don't take-off on taxiways. --- * Avoid to hit other planes on the airbase. --- * Obey ground control orders. --- --- 2) @{AirbasePolice#AIRBASEPOLICE_CAUCASUS} class, extends @{AirbasePolice#AIRBASEPOLICE_BASE} --- ============================================================================================= --- All the airbases on the caucasus map can be monitored using this class. --- If you want to monitor specific airbases, you need to use the @{#AIRBASEPOLICE_BASE.Monitor}() method, which takes a table or airbase names. --- The following names can be given: --- * AnapaVityazevo --- * Batumi --- * Beslan --- * Gelendzhik --- * Gudauta --- * Kobuleti --- * KrasnodarCenter --- * KrasnodarPashkovsky --- * Krymsk --- * Kutaisi --- * MaykopKhanskaya --- * MineralnyeVody --- * Mozdok --- * Nalchik --- * Novorossiysk --- * SenakiKolkhi --- * SochiAdler --- * Soganlug --- * SukhumiBabushara --- * TbilisiLochini --- * Vaziani --- --- 3) @{AirbasePolice#AIRBASEPOLICE_NEVADA} class, extends @{AirbasePolice#AIRBASEPOLICE_BASE} --- ============================================================================================= --- All the airbases on the NEVADA map can be monitored using this class. --- If you want to monitor specific airbases, you need to use the @{#AIRBASEPOLICE_BASE.Monitor}() method, which takes a table or airbase names. --- The following names can be given: --- * Nellis --- * McCarran --- * Creech --- * Groom Lake --- --- ### Contributions: Dutch Baron - Concept & Testing --- ### Author: FlightControl - Framework Design & Programming --- --- @module AirbasePolice - - - - - ---- @type AIRBASEPOLICE_BASE --- @field Core.Set#SET_CLIENT SetClient --- @extends Core.Base#BASE - -AIRBASEPOLICE_BASE = { - ClassName = "AIRBASEPOLICE_BASE", - SetClient = nil, - Airbases = nil, - AirbaseNames = nil, -} - - ---- Creates a new AIRBASEPOLICE_BASE object. --- @param #AIRBASEPOLICE_BASE self --- @param SetClient A SET_CLIENT object that will contain the CLIENT objects to be monitored if they follow the rules of the airbase. --- @param Airbases A table of Airbase Names. --- @return #AIRBASEPOLICE_BASE self -function AIRBASEPOLICE_BASE:New( SetClient, Airbases ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - self:E( { self.ClassName, SetClient, Airbases } ) - - self.SetClient = SetClient - self.Airbases = Airbases - - for AirbaseID, Airbase in pairs( self.Airbases ) do - Airbase.ZoneBoundary = ZONE_POLYGON_BASE:New( "Boundary", Airbase.PointsBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - for PointsRunwayID, PointsRunway in pairs( Airbase.PointsRunways ) do - Airbase.ZoneRunways[PointsRunwayID] = ZONE_POLYGON_BASE:New( "Runway " .. PointsRunwayID, PointsRunway ):SmokeZone(SMOKECOLOR.Red):Flush() - end - end - --- -- Template --- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) --- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) --- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - - self.SetClient:ForEachClient( - --- @param Wrapper.Client#CLIENT Client - function( Client ) - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0) - Client:SetState( self, "Taxi", false ) - end - ) - - self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, {}, 0, 2, 0.05 ) - - return self -end - ---- @type AIRBASEPOLICE_BASE.AirbaseNames --- @list <#string> - ---- Monitor a table of airbase names. --- @param #AIRBASEPOLICE_BASE self --- @param #AIRBASEPOLICE_BASE.AirbaseNames AirbaseNames A list of AirbaseNames to monitor. If this parameters is nil, then all airbases will be monitored. --- @return #AIRBASEPOLICE_BASE self -function AIRBASEPOLICE_BASE:Monitor( AirbaseNames ) - - if AirbaseNames then - if type( AirbaseNames ) == "table" then - self.AirbaseNames = AirbaseNames - else - self.AirbaseNames = { AirbaseNames } - end - end -end - ---- @param #AIRBASEPOLICE_BASE self -function AIRBASEPOLICE_BASE:_AirbaseMonitor() - - for AirbaseID, Airbase in pairs( self.Airbases ) do - - if not self.AirbaseNames or self.AirbaseNames[AirbaseID] then - - self:E( AirbaseID ) - - self.SetClient:ForEachClientInZone( Airbase.ZoneBoundary, - - --- @param Wrapper.Client#CLIENT Client - function( Client ) - - self:E( Client.UnitName ) - if Client:IsAlive() then - local NotInRunwayZone = true - for ZoneRunwayID, ZoneRunway in pairs( Airbase.ZoneRunways ) do - NotInRunwayZone = ( Client:IsNotInZone( ZoneRunway ) == true ) and NotInRunwayZone or false - end - - if NotInRunwayZone then - local Taxi = self:GetState( self, "Taxi" ) - self:E( Taxi ) - if Taxi == false then - Client:Message( "Welcome at " .. AirbaseID .. ". The maximum taxiing speed is " .. Airbase.MaximumSpeed " km/h.", 20, "ATC" ) - self:SetState( self, "Taxi", true ) - end - - -- TODO: GetVelocityKMH function usage - local VelocityVec3 = Client:GetVelocity() - local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec - local Velocity = Velocity * 3.6 -- now it is in km/h. - -- MESSAGE:New( "Velocity = " .. Velocity, 1 ):ToAll() - local IsAboveRunway = Client:IsAboveRunway() - local IsOnGround = Client:InAir() == false - self:T( IsAboveRunway, IsOnGround ) - - if IsAboveRunway and IsOnGround then - - if Velocity > Airbase.MaximumSpeed then - local IsSpeeding = Client:GetState( self, "Speeding" ) - - if IsSpeeding == true then - local SpeedingWarnings = Client:GetState( self, "Warnings" ) - self:T( SpeedingWarnings ) - - if SpeedingWarnings <= 3 then - Client:Message( "You are speeding on the taxiway! Slow down or you will be removed from this airbase! Your current velocity is " .. string.format( "%2.0f km/h", Velocity ), 5, "Warning " .. SpeedingWarnings .. " / 3" ) - Client:SetState( self, "Warnings", SpeedingWarnings + 1 ) - else - MESSAGE:New( "Player " .. Client:GetPlayerName() .. " is being damaged at the airbase, due to a speeding violation ...", 10, "Airbase Police" ):ToAll() - --- @param Wrapper.Client#CLIENT Client - local function DestroyUntilHeavilyDamaged( Client ) - local ClientCoord = Client:GetCoordinate() - ClientCoord:Explosion( 100 ) - local Damage = Client:GetLife() - local InitialLife = Client:GetLife0() - MESSAGE:New( "Player " .. Client:GetPlayerName() .. " Damage ... " .. Damage, 5, "Airbase Police" ):ToAll() - if ( Damage / InitialLife ) * 100 < 80 then - Client:ScheduleStop( DestroyUntilHeavilyDamaged ) - end - end - Client:ScheduleOnce( 1, DestroyUntilHeavilyDamaged, Client ) - --Client:ScheduleRepeat( 1, 1, 0, nil, DestroyUntilHeavilyDamaged, Client ) - --Client:Destroy() - trigger.action.setUserFlag( "AIRCRAFT_"..Client:GetID(), 100) - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0 ) - end - - else - Client:Message( "You are speeding on the taxiway, slow down now! Your current velocity is " .. string.format( "%2.0f km/h", Velocity ), 5, "Attention! " ) - Client:SetState( self, "Speeding", true ) - Client:SetState( self, "Warnings", 1 ) - end - - else - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0 ) - end - end - - else - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0 ) - local Taxi = self:GetState( self, "Taxi" ) - if Taxi == true then - Client:Message( "You have progressed to the runway ... Await take-off clearance ...", 20, "ATC" ) - self:SetState( self, "Taxi", false ) - end - end - end - end - ) - end - end - - return true -end - - ---- @type AIRBASEPOLICE_CAUCASUS --- @field Core.Set#SET_CLIENT SetClient --- @extends #AIRBASEPOLICE_BASE - -AIRBASEPOLICE_CAUCASUS = { - ClassName = "AIRBASEPOLICE_CAUCASUS", - Airbases = { - AnapaVityazevo = { - PointsBoundary = { - [1]={["y"]=242234.85714287,["x"]=-6616.5714285726,}, - [2]={["y"]=241060.57142858,["x"]=-5585.142857144,}, - [3]={["y"]=243806.2857143,["x"]=-3962.2857142868,}, - [4]={["y"]=245240.57142858,["x"]=-4816.5714285726,}, - [5]={["y"]=244783.42857144,["x"]=-5630.8571428583,}, - [6]={["y"]=243800.57142858,["x"]=-5065.142857144,}, - [7]={["y"]=242232.00000001,["x"]=-6622.2857142868,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=242140.57142858,["x"]=-6478.8571428583,}, - [2]={["y"]=242188.57142858,["x"]=-6522.0000000011,}, - [3]={["y"]=244124.2857143,["x"]=-4344.0000000011,}, - [4]={["y"]=244068.2857143,["x"]=-4296.5714285726,}, - [5]={["y"]=242140.57142858,["x"]=-6480.0000000011,} - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Batumi = { - PointsBoundary = { - [1]={["y"]=617567.14285714,["x"]=-355313.14285715,}, - [2]={["y"]=616181.42857142,["x"]=-354800.28571429,}, - [3]={["y"]=616007.14285714,["x"]=-355128.85714286,}, - [4]={["y"]=618230,["x"]=-356914.57142858,}, - [5]={["y"]=618727.14285714,["x"]=-356166,}, - [6]={["y"]=617572.85714285,["x"]=-355308.85714286,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=616442.28571429,["x"]=-355090.28571429,}, - [2]={["y"]=618450.57142857,["x"]=-356522,}, - [3]={["y"]=618407.71428571,["x"]=-356584.85714286,}, - [4]={["y"]=618361.99999999,["x"]=-356554.85714286,}, - [5]={["y"]=618324.85714285,["x"]=-356599.14285715,}, - [6]={["y"]=618250.57142856,["x"]=-356543.42857143,}, - [7]={["y"]=618257.7142857,["x"]=-356496.28571429,}, - [8]={["y"]=618237.7142857,["x"]=-356459.14285715,}, - [9]={["y"]=616555.71428571,["x"]=-355258.85714286,}, - [10]={["y"]=616486.28571428,["x"]=-355280.57142858,}, - [11]={["y"]=616410.57142856,["x"]=-355227.71428572,}, - [12]={["y"]=616441.99999999,["x"]=-355179.14285715,}, - [13]={["y"]=616401.99999999,["x"]=-355147.71428572,}, - [14]={["y"]=616441.42857142,["x"]=-355092.57142858,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Beslan = { - PointsBoundary = { - [1]={["y"]=842082.57142857,["x"]=-148445.14285715,}, - [2]={["y"]=845237.71428572,["x"]=-148639.71428572,}, - [3]={["y"]=845232,["x"]=-148765.42857143,}, - [4]={["y"]=844220.57142857,["x"]=-149168.28571429,}, - [5]={["y"]=843274.85714286,["x"]=-149125.42857143,}, - [6]={["y"]=842077.71428572,["x"]=-148554,}, - [7]={["y"]=842083.42857143,["x"]=-148445.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=842104.57142857,["x"]=-148460.57142857,}, - [2]={["y"]=845225.71428572,["x"]=-148656,}, - [3]={["y"]=845220.57142858,["x"]=-148750,}, - [4]={["y"]=842098.85714286,["x"]=-148556.28571429,}, - [5]={["y"]=842104,["x"]=-148460.28571429,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Gelendzhik = { - PointsBoundary = { - [1]={["y"]=297856.00000001,["x"]=-51151.428571429,}, - [2]={["y"]=299044.57142858,["x"]=-49720.000000001,}, - [3]={["y"]=298861.71428572,["x"]=-49580.000000001,}, - [4]={["y"]=298198.85714286,["x"]=-49842.857142858,}, - [5]={["y"]=297990.28571429,["x"]=-50151.428571429,}, - [6]={["y"]=297696.00000001,["x"]=-51054.285714286,}, - [7]={["y"]=297850.28571429,["x"]=-51160.000000001,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=297834.00000001,["x"]=-51107.428571429,}, - [2]={["y"]=297786.57142858,["x"]=-51068.857142858,}, - [3]={["y"]=298946.57142858,["x"]=-49686.000000001,}, - [4]={["y"]=298993.14285715,["x"]=-49725.714285715,}, - [5]={["y"]=297835.14285715,["x"]=-51107.714285715,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Gudauta = { - PointsBoundary = { - [1]={["y"]=517246.57142857,["x"]=-197850.28571429,}, - [2]={["y"]=516749.42857142,["x"]=-198070.28571429,}, - [3]={["y"]=515755.14285714,["x"]=-197598.85714286,}, - [4]={["y"]=515369.42857142,["x"]=-196538.85714286,}, - [5]={["y"]=515623.71428571,["x"]=-195618.85714286,}, - [6]={["y"]=515946.57142857,["x"]=-195510.28571429,}, - [7]={["y"]=517243.71428571,["x"]=-197858.85714286,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=517096.57142857,["x"]=-197804.57142857,}, - [2]={["y"]=515880.85714285,["x"]=-195590.28571429,}, - [3]={["y"]=515812.28571428,["x"]=-195628.85714286,}, - [4]={["y"]=517036.57142857,["x"]=-197834.57142857,}, - [5]={["y"]=517097.99999999,["x"]=-197807.42857143,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Kobuleti = { - PointsBoundary = { - [1]={["y"]=634427.71428571,["x"]=-318290.28571429,}, - [2]={["y"]=635033.42857143,["x"]=-317550.2857143,}, - [3]={["y"]=635864.85714286,["x"]=-317333.14285715,}, - [4]={["y"]=636967.71428571,["x"]=-317261.71428572,}, - [5]={["y"]=637144.85714286,["x"]=-317913.14285715,}, - [6]={["y"]=634630.57142857,["x"]=-318687.42857144,}, - [7]={["y"]=634424.85714286,["x"]=-318290.2857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=634509.71428571,["x"]=-318339.42857144,}, - [2]={["y"]=636767.42857143,["x"]=-317516.57142858,}, - [3]={["y"]=636790,["x"]=-317575.71428572,}, - [4]={["y"]=634531.42857143,["x"]=-318398.00000001,}, - [5]={["y"]=634510.28571429,["x"]=-318339.71428572,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - KrasnodarCenter = { - PointsBoundary = { - [1]={["y"]=366680.28571429,["x"]=11699.142857142,}, - [2]={["y"]=366654.28571429,["x"]=11225.142857142,}, - [3]={["y"]=367497.14285715,["x"]=11082.285714285,}, - [4]={["y"]=368025.71428572,["x"]=10396.57142857,}, - [5]={["y"]=369854.28571429,["x"]=11367.999999999,}, - [6]={["y"]=369840.00000001,["x"]=11910.857142856,}, - [7]={["y"]=366682.57142858,["x"]=11697.999999999,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=369205.42857144,["x"]=11789.142857142,}, - [2]={["y"]=369209.71428572,["x"]=11714.857142856,}, - [3]={["y"]=366699.71428572,["x"]=11581.714285713,}, - [4]={["y"]=366698.28571429,["x"]=11659.142857142,}, - [5]={["y"]=369208.85714286,["x"]=11788.57142857,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - KrasnodarPashkovsky = { - PointsBoundary = { - [1]={["y"]=386754,["x"]=6476.5714285703,}, - [2]={["y"]=389182.57142858,["x"]=8722.2857142846,}, - [3]={["y"]=388832.57142858,["x"]=9086.5714285703,}, - [4]={["y"]=386961.14285715,["x"]=7707.9999999989,}, - [5]={["y"]=385404,["x"]=9179.4285714274,}, - [6]={["y"]=383239.71428572,["x"]=7386.5714285703,}, - [7]={["y"]=383954,["x"]=6486.5714285703,}, - [8]={["y"]=385775.42857143,["x"]=8097.9999999989,}, - [9]={["y"]=386804,["x"]=7319.4285714274,}, - [10]={["y"]=386375.42857143,["x"]=6797.9999999989,}, - [11]={["y"]=386746.85714286,["x"]=6472.2857142846,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=385891.14285715,["x"]=8416.5714285703,}, - [2]={["y"]=385842.28571429,["x"]=8467.9999999989,}, - [3]={["y"]=384180.85714286,["x"]=6917.1428571417,}, - [4]={["y"]=384228.57142858,["x"]=6867.7142857132,}, - [5]={["y"]=385891.14285715,["x"]=8416.5714285703,}, - }, - [2] = { - [1]={["y"]=386714.85714286,["x"]=6674.857142856,}, - [2]={["y"]=386757.71428572,["x"]=6627.7142857132,}, - [3]={["y"]=389028.57142858,["x"]=8741.4285714275,}, - [4]={["y"]=388981.71428572,["x"]=8790.5714285703,}, - [5]={["y"]=386714.57142858,["x"]=6674.5714285703,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Krymsk = { - PointsBoundary = { - [1]={["y"]=293338.00000001,["x"]=-7575.4285714297,}, - [2]={["y"]=295199.42857144,["x"]=-5434.0000000011,}, - [3]={["y"]=295595.14285715,["x"]=-6239.7142857154,}, - [4]={["y"]=294152.2857143,["x"]=-8325.4285714297,}, - [5]={["y"]=293345.14285715,["x"]=-7596.8571428582,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=293522.00000001,["x"]=-7567.4285714297,}, - [2]={["y"]=293578.57142858,["x"]=-7616.0000000011,}, - [3]={["y"]=295246.00000001,["x"]=-5591.142857144,}, - [4]={["y"]=295187.71428573,["x"]=-5546.0000000011,}, - [5]={["y"]=293523.14285715,["x"]=-7568.2857142868,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Kutaisi = { - PointsBoundary = { - [1]={["y"]=682087.42857143,["x"]=-284512.85714286,}, - [2]={["y"]=685387.42857143,["x"]=-283662.85714286,}, - [3]={["y"]=685294.57142857,["x"]=-284977.14285715,}, - [4]={["y"]=682744.57142857,["x"]=-286505.71428572,}, - [5]={["y"]=682094.57142857,["x"]=-284527.14285715,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=682638,["x"]=-285202.28571429,}, - [2]={["y"]=685050.28571429,["x"]=-284507.42857144,}, - [3]={["y"]=685068.85714286,["x"]=-284578.85714286,}, - [4]={["y"]=682657.42857143,["x"]=-285264.28571429,}, - [5]={["y"]=682638.28571429,["x"]=-285202.85714286,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - MaykopKhanskaya = { - PointsBoundary = { - [1]={["y"]=456876.28571429,["x"]=-27665.42857143,}, - [2]={["y"]=457800,["x"]=-28392.857142858,}, - [3]={["y"]=459368.57142857,["x"]=-26378.571428573,}, - [4]={["y"]=459425.71428572,["x"]=-25242.857142858,}, - [5]={["y"]=458961.42857143,["x"]=-24964.285714287,}, - [6]={["y"]=456878.57142857,["x"]=-27667.714285715,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=457005.42857143,["x"]=-27668.000000001,}, - [2]={["y"]=459028.85714286,["x"]=-25168.857142858,}, - [3]={["y"]=459082.57142857,["x"]=-25216.857142858,}, - [4]={["y"]=457060,["x"]=-27714.285714287,}, - [5]={["y"]=457004.57142857,["x"]=-27669.714285715,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - MineralnyeVody = { - PointsBoundary = { - [1]={["y"]=703857.14285714,["x"]=-50226.000000002,}, - [2]={["y"]=707385.71428571,["x"]=-51911.714285716,}, - [3]={["y"]=707595.71428571,["x"]=-51434.857142859,}, - [4]={["y"]=707900,["x"]=-51568.857142859,}, - [5]={["y"]=707542.85714286,["x"]=-52326.000000002,}, - [6]={["y"]=706628.57142857,["x"]=-52568.857142859,}, - [7]={["y"]=705142.85714286,["x"]=-51790.285714288,}, - [8]={["y"]=703678.57142857,["x"]=-50611.714285716,}, - [9]={["y"]=703857.42857143,["x"]=-50226.857142859,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=703904,["x"]=-50352.571428573,}, - [2]={["y"]=707596.28571429,["x"]=-52094.571428573,}, - [3]={["y"]=707560.57142858,["x"]=-52161.714285716,}, - [4]={["y"]=703871.71428572,["x"]=-50420.571428573,}, - [5]={["y"]=703902,["x"]=-50352.000000002,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Mozdok = { - PointsBoundary = { - [1]={["y"]=832123.42857143,["x"]=-83608.571428573,}, - [2]={["y"]=835916.28571429,["x"]=-83144.285714288,}, - [3]={["y"]=835474.28571429,["x"]=-84170.571428573,}, - [4]={["y"]=832911.42857143,["x"]=-84470.571428573,}, - [5]={["y"]=832487.71428572,["x"]=-85565.714285716,}, - [6]={["y"]=831573.42857143,["x"]=-85351.42857143,}, - [7]={["y"]=832123.71428572,["x"]=-83610.285714288,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=832201.14285715,["x"]=-83699.428571431,}, - [2]={["y"]=832212.57142857,["x"]=-83780.571428574,}, - [3]={["y"]=835730.28571429,["x"]=-83335.714285717,}, - [4]={["y"]=835718.85714286,["x"]=-83246.571428574,}, - [5]={["y"]=832200.57142857,["x"]=-83700.000000002,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Nalchik = { - PointsBoundary = { - [1]={["y"]=759370,["x"]=-125502.85714286,}, - [2]={["y"]=761384.28571429,["x"]=-124177.14285714,}, - [3]={["y"]=761472.85714286,["x"]=-124325.71428572,}, - [4]={["y"]=761092.85714286,["x"]=-125048.57142857,}, - [5]={["y"]=760295.71428572,["x"]=-125685.71428572,}, - [6]={["y"]=759444.28571429,["x"]=-125734.28571429,}, - [7]={["y"]=759375.71428572,["x"]=-125511.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=759454.28571429,["x"]=-125551.42857143,}, - [2]={["y"]=759492.85714286,["x"]=-125610.85714286,}, - [3]={["y"]=761406.28571429,["x"]=-124304.28571429,}, - [4]={["y"]=761361.14285714,["x"]=-124239.71428572,}, - [5]={["y"]=759456,["x"]=-125552.57142857,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Novorossiysk = { - PointsBoundary = { - [1]={["y"]=278677.71428573,["x"]=-41656.571428572,}, - [2]={["y"]=278446.2857143,["x"]=-41453.714285715,}, - [3]={["y"]=278989.14285716,["x"]=-40188.000000001,}, - [4]={["y"]=279717.71428573,["x"]=-39968.000000001,}, - [5]={["y"]=280020.57142859,["x"]=-40208.000000001,}, - [6]={["y"]=278674.85714287,["x"]=-41660.857142858,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=278673.14285716,["x"]=-41615.142857144,}, - [2]={["y"]=278625.42857144,["x"]=-41570.571428572,}, - [3]={["y"]=279835.42857144,["x"]=-40226.000000001,}, - [4]={["y"]=279882.2857143,["x"]=-40270.000000001,}, - [5]={["y"]=278672.00000001,["x"]=-41614.857142858,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - SenakiKolkhi = { - PointsBoundary = { - [1]={["y"]=646036.57142857,["x"]=-281778.85714286,}, - [2]={["y"]=646045.14285714,["x"]=-281191.71428571,}, - [3]={["y"]=647032.28571429,["x"]=-280598.85714285,}, - [4]={["y"]=647669.42857143,["x"]=-281273.14285714,}, - [5]={["y"]=648323.71428571,["x"]=-281370.28571428,}, - [6]={["y"]=648520.85714286,["x"]=-281978.85714285,}, - [7]={["y"]=646039.42857143,["x"]=-281783.14285714,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=646060.85714285,["x"]=-281736,}, - [2]={["y"]=646056.57142857,["x"]=-281631.71428571,}, - [3]={["y"]=648442.28571428,["x"]=-281840.28571428,}, - [4]={["y"]=648432.28571428,["x"]=-281918.85714286,}, - [5]={["y"]=646063.71428571,["x"]=-281738.85714286,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - SochiAdler = { - PointsBoundary = { - [1]={["y"]=460642.28571428,["x"]=-164861.71428571,}, - [2]={["y"]=462820.85714285,["x"]=-163368.85714286,}, - [3]={["y"]=463649.42857142,["x"]=-163340.28571429,}, - [4]={["y"]=463835.14285714,["x"]=-164040.28571429,}, - [5]={["y"]=462535.14285714,["x"]=-165654.57142857,}, - [6]={["y"]=460678,["x"]=-165247.42857143,}, - [7]={["y"]=460635.14285714,["x"]=-164876,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=460831.42857143,["x"]=-165180,}, - [2]={["y"]=460878.57142857,["x"]=-165257.14285714,}, - [3]={["y"]=463663.71428571,["x"]=-163793.14285714,}, - [4]={["y"]=463612.28571428,["x"]=-163697.42857143,}, - [5]={["y"]=460831.42857143,["x"]=-165177.14285714,}, - }, - [2] = { - [1]={["y"]=460831.42857143,["x"]=-165180,}, - [2]={["y"]=460878.57142857,["x"]=-165257.14285714,}, - [3]={["y"]=463663.71428571,["x"]=-163793.14285714,}, - [4]={["y"]=463612.28571428,["x"]=-163697.42857143,}, - [5]={["y"]=460831.42857143,["x"]=-165177.14285714,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Soganlug = { - PointsBoundary = { - [1]={["y"]=894530.85714286,["x"]=-316928.28571428,}, - [2]={["y"]=896422.28571428,["x"]=-318622.57142857,}, - [3]={["y"]=896090.85714286,["x"]=-318934,}, - [4]={["y"]=894019.42857143,["x"]=-317119.71428571,}, - [5]={["y"]=894533.71428571,["x"]=-316925.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=894525.71428571,["x"]=-316964,}, - [2]={["y"]=896363.14285714,["x"]=-318634.28571428,}, - [3]={["y"]=896299.14285714,["x"]=-318702.85714286,}, - [4]={["y"]=894464,["x"]=-317031.71428571,}, - [5]={["y"]=894524.57142857,["x"]=-316963.71428571,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - SukhumiBabushara = { - PointsBoundary = { - [1]={["y"]=562541.14285714,["x"]=-219852.28571429,}, - [2]={["y"]=562691.14285714,["x"]=-219395.14285714,}, - [3]={["y"]=564326.85714286,["x"]=-219523.71428571,}, - [4]={["y"]=566262.57142857,["x"]=-221166.57142857,}, - [5]={["y"]=566069.71428571,["x"]=-221580.85714286,}, - [6]={["y"]=562534,["x"]=-219873.71428571,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=562684,["x"]=-219779.71428571,}, - [2]={["y"]=562717.71428571,["x"]=-219718,}, - [3]={["y"]=566046.85714286,["x"]=-221376.57142857,}, - [4]={["y"]=566012.28571428,["x"]=-221446.57142857,}, - [5]={["y"]=562684.57142857,["x"]=-219782.57142857,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - TbilisiLochini = { - PointsBoundary = { - [1]={["y"]=895172.85714286,["x"]=-314667.42857143,}, - [2]={["y"]=895337.42857143,["x"]=-314143.14285714,}, - [3]={["y"]=895990.28571429,["x"]=-314036,}, - [4]={["y"]=897730.28571429,["x"]=-315284.57142857,}, - [5]={["y"]=897901.71428571,["x"]=-316284.57142857,}, - [6]={["y"]=897684.57142857,["x"]=-316618.85714286,}, - [7]={["y"]=895173.14285714,["x"]=-314667.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=895261.14285715,["x"]=-314652.28571428,}, - [2]={["y"]=897654.57142857,["x"]=-316523.14285714,}, - [3]={["y"]=897711.71428571,["x"]=-316450.28571429,}, - [4]={["y"]=895327.42857143,["x"]=-314568.85714286,}, - [5]={["y"]=895261.71428572,["x"]=-314656,}, - }, - [2] = { - [1]={["y"]=895605.71428572,["x"]=-314724.57142857,}, - [2]={["y"]=897639.71428572,["x"]=-316148,}, - [3]={["y"]=897683.42857143,["x"]=-316087.14285714,}, - [4]={["y"]=895650,["x"]=-314660,}, - [5]={["y"]=895606,["x"]=-314724.85714286,} - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Vaziani = { - PointsBoundary = { - [1]={["y"]=902122,["x"]=-318163.71428572,}, - [2]={["y"]=902678.57142857,["x"]=-317594,}, - [3]={["y"]=903275.71428571,["x"]=-317405.42857143,}, - [4]={["y"]=903418.57142857,["x"]=-317891.14285714,}, - [5]={["y"]=904292.85714286,["x"]=-318748.28571429,}, - [6]={["y"]=904542,["x"]=-319740.85714286,}, - [7]={["y"]=904042,["x"]=-320166.57142857,}, - [8]={["y"]=902121.42857143,["x"]=-318164.85714286,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=902239.14285714,["x"]=-318190.85714286,}, - [2]={["y"]=904014.28571428,["x"]=-319994.57142857,}, - [3]={["y"]=904064.85714285,["x"]=-319945.14285715,}, - [4]={["y"]=902294.57142857,["x"]=-318146,}, - [5]={["y"]=902247.71428571,["x"]=-318190.85714286,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - }, -} - ---- Creates a new AIRBASEPOLICE_CAUCASUS object. --- @param #AIRBASEPOLICE_CAUCASUS self --- @param SetClient A SET_CLIENT object that will contain the CLIENT objects to be monitored if they follow the rules of the airbase. --- @return #AIRBASEPOLICE_CAUCASUS self -function AIRBASEPOLICE_CAUCASUS:New( SetClient ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AIRBASEPOLICE_BASE:New( SetClient, self.Airbases ) ) - - -- -- AnapaVityazevo - -- local AnapaVityazevoBoundary = GROUP:FindByName( "AnapaVityazevo Boundary" ) - -- self.Airbases.AnapaVityazevo.ZoneBoundary = ZONE_POLYGON:New( "AnapaVityazevo Boundary", AnapaVityazevoBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local AnapaVityazevoRunway1 = GROUP:FindByName( "AnapaVityazevo Runway 1" ) - -- self.Airbases.AnapaVityazevo.ZoneRunways[1] = ZONE_POLYGON:New( "AnapaVityazevo Runway 1", AnapaVityazevoRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Batumi - -- local BatumiBoundary = GROUP:FindByName( "Batumi Boundary" ) - -- self.Airbases.Batumi.ZoneBoundary = ZONE_POLYGON:New( "Batumi Boundary", BatumiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local BatumiRunway1 = GROUP:FindByName( "Batumi Runway 1" ) - -- self.Airbases.Batumi.ZoneRunways[1] = ZONE_POLYGON:New( "Batumi Runway 1", BatumiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Beslan - -- local BeslanBoundary = GROUP:FindByName( "Beslan Boundary" ) - -- self.Airbases.Beslan.ZoneBoundary = ZONE_POLYGON:New( "Beslan Boundary", BeslanBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local BeslanRunway1 = GROUP:FindByName( "Beslan Runway 1" ) - -- self.Airbases.Beslan.ZoneRunways[1] = ZONE_POLYGON:New( "Beslan Runway 1", BeslanRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Gelendzhik - -- local GelendzhikBoundary = GROUP:FindByName( "Gelendzhik Boundary" ) - -- self.Airbases.Gelendzhik.ZoneBoundary = ZONE_POLYGON:New( "Gelendzhik Boundary", GelendzhikBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local GelendzhikRunway1 = GROUP:FindByName( "Gelendzhik Runway 1" ) - -- self.Airbases.Gelendzhik.ZoneRunways[1] = ZONE_POLYGON:New( "Gelendzhik Runway 1", GelendzhikRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Gudauta - -- local GudautaBoundary = GROUP:FindByName( "Gudauta Boundary" ) - -- self.Airbases.Gudauta.ZoneBoundary = ZONE_POLYGON:New( "Gudauta Boundary", GudautaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local GudautaRunway1 = GROUP:FindByName( "Gudauta Runway 1" ) - -- self.Airbases.Gudauta.ZoneRunways[1] = ZONE_POLYGON:New( "Gudauta Runway 1", GudautaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Kobuleti - -- local KobuletiBoundary = GROUP:FindByName( "Kobuleti Boundary" ) - -- self.Airbases.Kobuleti.ZoneBoundary = ZONE_POLYGON:New( "Kobuleti Boundary", KobuletiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KobuletiRunway1 = GROUP:FindByName( "Kobuleti Runway 1" ) - -- self.Airbases.Kobuleti.ZoneRunways[1] = ZONE_POLYGON:New( "Kobuleti Runway 1", KobuletiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- KrasnodarCenter - -- local KrasnodarCenterBoundary = GROUP:FindByName( "KrasnodarCenter Boundary" ) - -- self.Airbases.KrasnodarCenter.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarCenter Boundary", KrasnodarCenterBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KrasnodarCenterRunway1 = GROUP:FindByName( "KrasnodarCenter Runway 1" ) - -- self.Airbases.KrasnodarCenter.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarCenter Runway 1", KrasnodarCenterRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- KrasnodarPashkovsky - -- local KrasnodarPashkovskyBoundary = GROUP:FindByName( "KrasnodarPashkovsky Boundary" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarPashkovsky Boundary", KrasnodarPashkovskyBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KrasnodarPashkovskyRunway1 = GROUP:FindByName( "KrasnodarPashkovsky Runway 1" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 1", KrasnodarPashkovskyRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- local KrasnodarPashkovskyRunway2 = GROUP:FindByName( "KrasnodarPashkovsky Runway 2" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[2] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 2", KrasnodarPashkovskyRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Krymsk - -- local KrymskBoundary = GROUP:FindByName( "Krymsk Boundary" ) - -- self.Airbases.Krymsk.ZoneBoundary = ZONE_POLYGON:New( "Krymsk Boundary", KrymskBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KrymskRunway1 = GROUP:FindByName( "Krymsk Runway 1" ) - -- self.Airbases.Krymsk.ZoneRunways[1] = ZONE_POLYGON:New( "Krymsk Runway 1", KrymskRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Kutaisi - -- local KutaisiBoundary = GROUP:FindByName( "Kutaisi Boundary" ) - -- self.Airbases.Kutaisi.ZoneBoundary = ZONE_POLYGON:New( "Kutaisi Boundary", KutaisiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KutaisiRunway1 = GROUP:FindByName( "Kutaisi Runway 1" ) - -- self.Airbases.Kutaisi.ZoneRunways[1] = ZONE_POLYGON:New( "Kutaisi Runway 1", KutaisiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- MaykopKhanskaya - -- local MaykopKhanskayaBoundary = GROUP:FindByName( "MaykopKhanskaya Boundary" ) - -- self.Airbases.MaykopKhanskaya.ZoneBoundary = ZONE_POLYGON:New( "MaykopKhanskaya Boundary", MaykopKhanskayaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local MaykopKhanskayaRunway1 = GROUP:FindByName( "MaykopKhanskaya Runway 1" ) - -- self.Airbases.MaykopKhanskaya.ZoneRunways[1] = ZONE_POLYGON:New( "MaykopKhanskaya Runway 1", MaykopKhanskayaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- MineralnyeVody - -- local MineralnyeVodyBoundary = GROUP:FindByName( "MineralnyeVody Boundary" ) - -- self.Airbases.MineralnyeVody.ZoneBoundary = ZONE_POLYGON:New( "MineralnyeVody Boundary", MineralnyeVodyBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local MineralnyeVodyRunway1 = GROUP:FindByName( "MineralnyeVody Runway 1" ) - -- self.Airbases.MineralnyeVody.ZoneRunways[1] = ZONE_POLYGON:New( "MineralnyeVody Runway 1", MineralnyeVodyRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Mozdok - -- local MozdokBoundary = GROUP:FindByName( "Mozdok Boundary" ) - -- self.Airbases.Mozdok.ZoneBoundary = ZONE_POLYGON:New( "Mozdok Boundary", MozdokBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local MozdokRunway1 = GROUP:FindByName( "Mozdok Runway 1" ) - -- self.Airbases.Mozdok.ZoneRunways[1] = ZONE_POLYGON:New( "Mozdok Runway 1", MozdokRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Nalchik - -- local NalchikBoundary = GROUP:FindByName( "Nalchik Boundary" ) - -- self.Airbases.Nalchik.ZoneBoundary = ZONE_POLYGON:New( "Nalchik Boundary", NalchikBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local NalchikRunway1 = GROUP:FindByName( "Nalchik Runway 1" ) - -- self.Airbases.Nalchik.ZoneRunways[1] = ZONE_POLYGON:New( "Nalchik Runway 1", NalchikRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Novorossiysk - -- local NovorossiyskBoundary = GROUP:FindByName( "Novorossiysk Boundary" ) - -- self.Airbases.Novorossiysk.ZoneBoundary = ZONE_POLYGON:New( "Novorossiysk Boundary", NovorossiyskBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local NovorossiyskRunway1 = GROUP:FindByName( "Novorossiysk Runway 1" ) - -- self.Airbases.Novorossiysk.ZoneRunways[1] = ZONE_POLYGON:New( "Novorossiysk Runway 1", NovorossiyskRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- SenakiKolkhi - -- local SenakiKolkhiBoundary = GROUP:FindByName( "SenakiKolkhi Boundary" ) - -- self.Airbases.SenakiKolkhi.ZoneBoundary = ZONE_POLYGON:New( "SenakiKolkhi Boundary", SenakiKolkhiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SenakiKolkhiRunway1 = GROUP:FindByName( "SenakiKolkhi Runway 1" ) - -- self.Airbases.SenakiKolkhi.ZoneRunways[1] = ZONE_POLYGON:New( "SenakiKolkhi Runway 1", SenakiKolkhiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- SochiAdler - -- local SochiAdlerBoundary = GROUP:FindByName( "SochiAdler Boundary" ) - -- self.Airbases.SochiAdler.ZoneBoundary = ZONE_POLYGON:New( "SochiAdler Boundary", SochiAdlerBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SochiAdlerRunway1 = GROUP:FindByName( "SochiAdler Runway 1" ) - -- self.Airbases.SochiAdler.ZoneRunways[1] = ZONE_POLYGON:New( "SochiAdler Runway 1", SochiAdlerRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- local SochiAdlerRunway2 = GROUP:FindByName( "SochiAdler Runway 2" ) - -- self.Airbases.SochiAdler.ZoneRunways[2] = ZONE_POLYGON:New( "SochiAdler Runway 2", SochiAdlerRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Soganlug - -- local SoganlugBoundary = GROUP:FindByName( "Soganlug Boundary" ) - -- self.Airbases.Soganlug.ZoneBoundary = ZONE_POLYGON:New( "Soganlug Boundary", SoganlugBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SoganlugRunway1 = GROUP:FindByName( "Soganlug Runway 1" ) - -- self.Airbases.Soganlug.ZoneRunways[1] = ZONE_POLYGON:New( "Soganlug Runway 1", SoganlugRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- SukhumiBabushara - -- local SukhumiBabusharaBoundary = GROUP:FindByName( "SukhumiBabushara Boundary" ) - -- self.Airbases.SukhumiBabushara.ZoneBoundary = ZONE_POLYGON:New( "SukhumiBabushara Boundary", SukhumiBabusharaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SukhumiBabusharaRunway1 = GROUP:FindByName( "SukhumiBabushara Runway 1" ) - -- self.Airbases.SukhumiBabushara.ZoneRunways[1] = ZONE_POLYGON:New( "SukhumiBabushara Runway 1", SukhumiBabusharaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- TbilisiLochini - -- local TbilisiLochiniBoundary = GROUP:FindByName( "TbilisiLochini Boundary" ) - -- self.Airbases.TbilisiLochini.ZoneBoundary = ZONE_POLYGON:New( "TbilisiLochini Boundary", TbilisiLochiniBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local TbilisiLochiniRunway1 = GROUP:FindByName( "TbilisiLochini Runway 1" ) - -- self.Airbases.TbilisiLochini.ZoneRunways[1] = ZONE_POLYGON:New( "TbilisiLochini Runway 1", TbilisiLochiniRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- local TbilisiLochiniRunway2 = GROUP:FindByName( "TbilisiLochini Runway 2" ) - -- self.Airbases.TbilisiLochini.ZoneRunways[2] = ZONE_POLYGON:New( "TbilisiLochini Runway 2", TbilisiLochiniRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Vaziani - -- local VazianiBoundary = GROUP:FindByName( "Vaziani Boundary" ) - -- self.Airbases.Vaziani.ZoneBoundary = ZONE_POLYGON:New( "Vaziani Boundary", VazianiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local VazianiRunway1 = GROUP:FindByName( "Vaziani Runway 1" ) - -- self.Airbases.Vaziani.ZoneRunways[1] = ZONE_POLYGON:New( "Vaziani Runway 1", VazianiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - - - -- Template - -- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) - -- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) - -- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - - return self - -end - - - - ---- @type AIRBASEPOLICE_NEVADA --- @extends Functional.AirbasePolice#AIRBASEPOLICE_BASE -AIRBASEPOLICE_NEVADA = { - ClassName = "AIRBASEPOLICE_NEVADA", - Airbases = { - Nellis = { - PointsBoundary = { - [1]={["y"]=-17814.714285714,["x"]=-399823.14285714,}, - [2]={["y"]=-16875.857142857,["x"]=-398763.14285714,}, - [3]={["y"]=-16251.571428571,["x"]=-398988.85714286,}, - [4]={["y"]=-16163,["x"]=-398693.14285714,}, - [5]={["y"]=-16328.714285714,["x"]=-398034.57142857,}, - [6]={["y"]=-15943,["x"]=-397571.71428571,}, - [7]={["y"]=-15711.571428571,["x"]=-397551.71428571,}, - [8]={["y"]=-15748.714285714,["x"]=-396806,}, - [9]={["y"]=-16288.714285714,["x"]=-396517.42857143,}, - [10]={["y"]=-16751.571428571,["x"]=-396308.85714286,}, - [11]={["y"]=-17263,["x"]=-396234.57142857,}, - [12]={["y"]=-17577.285714286,["x"]=-396640.28571429,}, - [13]={["y"]=-17614.428571429,["x"]=-397400.28571429,}, - [14]={["y"]=-19405.857142857,["x"]=-399428.85714286,}, - [15]={["y"]=-19234.428571429,["x"]=-399683.14285714,}, - [16]={["y"]=-18708.714285714,["x"]=-399408.85714286,}, - [17]={["y"]=-18397.285714286,["x"]=-399657.42857143,}, - [18]={["y"]=-17814.428571429,["x"]=-399823.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=-18687,["x"]=-399380.28571429,}, - [2]={["y"]=-18620.714285714,["x"]=-399436.85714286,}, - [3]={["y"]=-16217.857142857,["x"]=-396596.85714286,}, - [4]={["y"]=-16300.142857143,["x"]=-396530,}, - [5]={["y"]=-18687,["x"]=-399380.85714286,}, - }, - [2] = { - [1]={["y"]=-18451.571428572,["x"]=-399580.57142857,}, - [2]={["y"]=-18392.142857143,["x"]=-399628.57142857,}, - [3]={["y"]=-16011,["x"]=-396806.85714286,}, - [4]={["y"]=-16074.714285714,["x"]=-396751.71428572,}, - [5]={["y"]=-18451.571428572,["x"]=-399580.85714285,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - McCarran = { - PointsBoundary = { - [1]={["y"]=-29455.285714286,["x"]=-416277.42857142,}, - [2]={["y"]=-28860.142857143,["x"]=-416492,}, - [3]={["y"]=-25044.428571429,["x"]=-416344.85714285,}, - [4]={["y"]=-24580.142857143,["x"]=-415959.14285714,}, - [5]={["y"]=-25073,["x"]=-415630.57142857,}, - [6]={["y"]=-25087.285714286,["x"]=-415130.57142857,}, - [7]={["y"]=-25830.142857143,["x"]=-414866.28571428,}, - [8]={["y"]=-26658.714285715,["x"]=-414880.57142857,}, - [9]={["y"]=-26973,["x"]=-415273.42857142,}, - [10]={["y"]=-27380.142857143,["x"]=-415187.71428571,}, - [11]={["y"]=-27715.857142857,["x"]=-414144.85714285,}, - [12]={["y"]=-27551.571428572,["x"]=-413473.42857142,}, - [13]={["y"]=-28630.142857143,["x"]=-413201.99999999,}, - [14]={["y"]=-29494.428571429,["x"]=-415437.71428571,}, - [15]={["y"]=-29455.571428572,["x"]=-416277.71428571,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=-29408.428571429,["x"]=-416016.28571428,}, - [2]={["y"]=-29408.142857144,["x"]=-416105.42857142,}, - [3]={["y"]=-24680.714285715,["x"]=-416003.14285713,}, - [4]={["y"]=-24681.857142858,["x"]=-415926.57142856,}, - [5]={["y"]=-29408.42857143,["x"]=-416016.57142856,}, - }, - [2] = { - [1]={["y"]=-28575.571428572,["x"]=-416303.14285713,}, - [2]={["y"]=-28575.571428572,["x"]=-416382.57142856,}, - [3]={["y"]=-25111.000000001,["x"]=-416309.7142857,}, - [4]={["y"]=-25111.000000001,["x"]=-416249.14285713,}, - [5]={["y"]=-28575.571428572,["x"]=-416303.7142857,}, - }, - [3] = { - [1]={["y"]=-29331.000000001,["x"]=-416275.42857141,}, - [2]={["y"]=-29259.000000001,["x"]=-416306.85714284,}, - [3]={["y"]=-28005.571428572,["x"]=-413449.7142857,}, - [4]={["y"]=-28068.714285715,["x"]=-413422.85714284,}, - [5]={["y"]=-29331.000000001,["x"]=-416275.7142857,}, - }, - [4] = { - [1]={["y"]=-29073.285714286,["x"]=-416386.57142856,}, - [2]={["y"]=-28997.285714286,["x"]=-416417.42857141,}, - [3]={["y"]=-27697.571428572,["x"]=-413464.57142856,}, - [4]={["y"]=-27767.857142858,["x"]=-413434.28571427,}, - [5]={["y"]=-29073.000000001,["x"]=-416386.85714284,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Creech = { - PointsBoundary = { - [1]={["y"]=-74522.714285715,["x"]=-360887.99999998,}, - [2]={["y"]=-74197,["x"]=-360556.57142855,}, - [3]={["y"]=-74402.714285715,["x"]=-359639.42857141,}, - [4]={["y"]=-74637,["x"]=-359279.42857141,}, - [5]={["y"]=-75759.857142857,["x"]=-359005.14285712,}, - [6]={["y"]=-75834.142857143,["x"]=-359045.14285712,}, - [7]={["y"]=-75902.714285714,["x"]=-359782.28571427,}, - [8]={["y"]=-76099.857142857,["x"]=-360399.42857141,}, - [9]={["y"]=-77314.142857143,["x"]=-360219.42857141,}, - [10]={["y"]=-77728.428571429,["x"]=-360445.14285713,}, - [11]={["y"]=-77585.571428571,["x"]=-360585.14285713,}, - [12]={["y"]=-76471.285714286,["x"]=-360819.42857141,}, - [13]={["y"]=-76325.571428571,["x"]=-360942.28571427,}, - [14]={["y"]=-74671.857142857,["x"]=-360927.7142857,}, - [15]={["y"]=-74522.714285714,["x"]=-360888.85714284,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=-74237.571428571,["x"]=-360591.7142857,}, - [2]={["y"]=-74234.428571429,["x"]=-360493.71428571,}, - [3]={["y"]=-77605.285714286,["x"]=-360399.14285713,}, - [4]={["y"]=-77608.714285715,["x"]=-360498.85714285,}, - [5]={["y"]=-74237.857142857,["x"]=-360591.7142857,}, - }, - [2] = { - [1]={["y"]=-75807.571428572,["x"]=-359073.42857142,}, - [2]={["y"]=-74770.142857144,["x"]=-360581.71428571,}, - [3]={["y"]=-74641.285714287,["x"]=-360585.42857142,}, - [4]={["y"]=-75734.142857144,["x"]=-359023.14285714,}, - [5]={["y"]=-75807.285714287,["x"]=-359073.42857142,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - GroomLake = { - PointsBoundary = { - [1]={["y"]=-88916.714285714,["x"]=-289102.28571425,}, - [2]={["y"]=-87023.571428572,["x"]=-290388.57142857,}, - [3]={["y"]=-85916.428571429,["x"]=-290674.28571428,}, - [4]={["y"]=-87645.000000001,["x"]=-286567.14285714,}, - [5]={["y"]=-88380.714285715,["x"]=-286388.57142857,}, - [6]={["y"]=-89670.714285715,["x"]=-283524.28571428,}, - [7]={["y"]=-89797.857142858,["x"]=-283567.14285714,}, - [8]={["y"]=-88635.000000001,["x"]=-286749.99999999,}, - [9]={["y"]=-89177.857142858,["x"]=-287207.14285714,}, - [10]={["y"]=-89092.142857144,["x"]=-288892.85714285,}, - [11]={["y"]=-88917.000000001,["x"]=-289102.85714285,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=-86039.000000001,["x"]=-290606.28571428,}, - [2]={["y"]=-85965.285714287,["x"]=-290573.99999999,}, - [3]={["y"]=-87692.714285715,["x"]=-286634.85714285,}, - [4]={["y"]=-87756.714285715,["x"]=-286663.99999999,}, - [5]={["y"]=-86038.714285715,["x"]=-290606.85714285,}, - }, - [2] = { - [1]={["y"]=-86808.428571429,["x"]=-290375.7142857,}, - [2]={["y"]=-86732.714285715,["x"]=-290344.28571427,}, - [3]={["y"]=-89672.714285714,["x"]=-283546.57142855,}, - [4]={["y"]=-89772.142857143,["x"]=-283587.71428569,}, - [5]={["y"]=-86808.142857143,["x"]=-290375.7142857,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - }, -} - ---- Creates a new AIRBASEPOLICE_NEVADA object. --- @param #AIRBASEPOLICE_NEVADA self --- @param SetClient A SET_CLIENT object that will contain the CLIENT objects to be monitored if they follow the rules of the airbase. --- @return #AIRBASEPOLICE_NEVADA self -function AIRBASEPOLICE_NEVADA:New( SetClient ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AIRBASEPOLICE_BASE:New( SetClient, self.Airbases ) ) - --- -- Nellis --- local NellisBoundary = GROUP:FindByName( "Nellis Boundary" ) --- self.Airbases.Nellis.ZoneBoundary = ZONE_POLYGON:New( "Nellis Boundary", NellisBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local NellisRunway1 = GROUP:FindByName( "Nellis Runway 1" ) --- self.Airbases.Nellis.ZoneRunways[1] = ZONE_POLYGON:New( "Nellis Runway 1", NellisRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local NellisRunway2 = GROUP:FindByName( "Nellis Runway 2" ) --- self.Airbases.Nellis.ZoneRunways[2] = ZONE_POLYGON:New( "Nellis Runway 2", NellisRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- -- McCarran --- local McCarranBoundary = GROUP:FindByName( "McCarran Boundary" ) --- self.Airbases.McCarran.ZoneBoundary = ZONE_POLYGON:New( "McCarran Boundary", McCarranBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local McCarranRunway1 = GROUP:FindByName( "McCarran Runway 1" ) --- self.Airbases.McCarran.ZoneRunways[1] = ZONE_POLYGON:New( "McCarran Runway 1", McCarranRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local McCarranRunway2 = GROUP:FindByName( "McCarran Runway 2" ) --- self.Airbases.McCarran.ZoneRunways[2] = ZONE_POLYGON:New( "McCarran Runway 2", McCarranRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local McCarranRunway3 = GROUP:FindByName( "McCarran Runway 3" ) --- self.Airbases.McCarran.ZoneRunways[3] = ZONE_POLYGON:New( "McCarran Runway 3", McCarranRunway3 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local McCarranRunway4 = GROUP:FindByName( "McCarran Runway 4" ) --- self.Airbases.McCarran.ZoneRunways[4] = ZONE_POLYGON:New( "McCarran Runway 4", McCarranRunway4 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- -- Creech --- local CreechBoundary = GROUP:FindByName( "Creech Boundary" ) --- self.Airbases.Creech.ZoneBoundary = ZONE_POLYGON:New( "Creech Boundary", CreechBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local CreechRunway1 = GROUP:FindByName( "Creech Runway 1" ) --- self.Airbases.Creech.ZoneRunways[1] = ZONE_POLYGON:New( "Creech Runway 1", CreechRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local CreechRunway2 = GROUP:FindByName( "Creech Runway 2" ) --- self.Airbases.Creech.ZoneRunways[2] = ZONE_POLYGON:New( "Creech Runway 2", CreechRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- -- Groom Lake --- local GroomLakeBoundary = GROUP:FindByName( "GroomLake Boundary" ) --- self.Airbases.GroomLake.ZoneBoundary = ZONE_POLYGON:New( "GroomLake Boundary", GroomLakeBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local GroomLakeRunway1 = GROUP:FindByName( "GroomLake Runway 1" ) --- self.Airbases.GroomLake.ZoneRunways[1] = ZONE_POLYGON:New( "GroomLake Runway 1", GroomLakeRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local GroomLakeRunway2 = GROUP:FindByName( "GroomLake Runway 2" ) --- self.Airbases.GroomLake.ZoneRunways[2] = ZONE_POLYGON:New( "GroomLake Runway 2", GroomLakeRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - -end - - - - - - --- **Functional** -- DETECTION_ classes model the detection of enemy units by FACs or RECCEs and group them according various methods. --- --- ![Banner Image](..\Presentations\DETECTION\Dia1.JPG) --- --- === --- --- DETECTION classes facilitate the detection of enemy units within the battle zone executed by FACs (Forward Air Controllers) or RECCEs (Reconnassance Units). --- DETECTION uses the in-built detection capabilities of DCS World, but adds new functionalities. --- --- Find the DETECTION classes documentation further in this document in the globals section. --- --- ==== --- --- # Demo Missions --- --- ### [DETECTION Demo Missions and Source Code](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/DET%20-%20Detection) --- --- ### [DETECTION Demo Missions, only for Beta Testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/DET%20-%20Detection) --- --- ### [ALL Demo Missions pack of the Latest Release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) --- --- ==== --- --- # YouTube Channel --- --- ### [DETECTION YouTube Channel](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl3Cf5jpI6BS0sBOVWK__tji) --- --- ==== --- --- ### Contributions: --- --- * Mechanist : Early concept of DETECTION_AREAS. --- --- ### Authors: --- --- * FlightControl : Analysis, Design, Programming, Testing --- --- @module Detection - -----BASE:TraceClass("DETECTION_BASE") -----BASE:TraceClass("DETECTION_AREAS") -----BASE:TraceClass("DETECTION_UNITS") -----BASE:TraceClass("DETECTION_TYPES") - -do -- DETECTION_BASE - - --- @type DETECTION_BASE - -- @field Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. - -- @field Dcs.DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected. - -- @field #DETECTION_BASE.DetectedObjects DetectedObjects The list of detected objects. - -- @field #table DetectedObjectsIdentified Map of the DetectedObjects identified. - -- @field #number DetectionRun - -- @extends Core.Fsm#FSM - - --- DETECTION_BASE class, extends @{Fsm#FSM} - -- - -- The DETECTION_BASE class defines the core functions to administer detected objects. - -- The DETECTION_BASE class will detect objects within the battle zone for a list of @{Group}s detecting targets following (a) detection method(s). - -- - -- ## DETECTION_BASE constructor - -- - -- Construct a new DETECTION_BASE instance using the @{#DETECTION_BASE.New}() method. - -- - -- ## Initialization - -- - -- By default, detection will return detected objects with all the detection sensors available. - -- However, you can ask how the objects were found with specific detection methods. - -- If you use one of the below methods, the detection will work with the detection method specified. - -- You can specify to apply multiple detection methods. - -- - -- Use the following functions to report the objects it detected using the methods Visual, Optical, Radar, IRST, RWR, DLINK: - -- - -- * @{#DETECTION_BASE.InitDetectVisual}(): Detected using Visual. - -- * @{#DETECTION_BASE.InitDetectOptical}(): Detected using Optical. - -- * @{#DETECTION_BASE.InitDetectRadar}(): Detected using Radar. - -- * @{#DETECTION_BASE.InitDetectIRST}(): Detected using IRST. - -- * @{#DETECTION_BASE.InitDetectRWR}(): Detected using RWR. - -- * @{#DETECTION_BASE.InitDetectDLINK}(): Detected using DLINK. - -- - -- ## **Filter** detected units based on **category of the unit** - -- - -- Filter the detected units based on Unit.Category using the method @{#DETECTION_BASE.FilterCategories}(). - -- The different values of Unit.Category can be: - -- - -- * Unit.Category.AIRPLANE - -- * Unit.Category.GROUND_UNIT - -- * Unit.Category.HELICOPTER - -- * Unit.Category.SHIP - -- * Unit.Category.STRUCTURE - -- - -- Multiple Unit.Category entries can be given as a table and then these will be evaluated as an OR expression. - -- - -- Example to filter a single category (Unit.Category.AIRPLANE). - -- - -- DetectionObject:FilterCategories( Unit.Category.AIRPLANE ) - -- - -- Example to filter multiple categories (Unit.Category.AIRPLANE, Unit.Category.HELICOPTER). Note the {}. - -- - -- DetectionObject:FilterCategories( { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) - -- - -- - -- ## **DETECTION_ derived classes** group the detected units into a **DetectedItems[]** list - -- - -- DETECTION_BASE derived classes build a list called DetectedItems[], which is essentially a first later - -- of grouping of detected units. Each DetectedItem within the DetectedItems[] list contains - -- a SET_UNIT object that contains the detected units that belong to that group. - -- - -- Derived classes will apply different methods to group the detected units. - -- Examples are per area, per quadrant, per distance, per type. - -- See further the derived DETECTION classes on which grouping methods are currently supported. - -- - -- Various methods exist how to retrieve the grouped items from a DETECTION_BASE derived class: - -- - -- * The method @{Detection#DETECTION_BASE.GetDetectedItems}() retrieves the DetectedItems[] list. - -- * A DetectedItem from the DetectedItems[] list can be retrieved using the method @{Detection#DETECTION_BASE.GetDetectedItem}( DetectedItemIndex ). - -- Note that this method returns a DetectedItem element from the list, that contains a Set variable and further information - -- about the DetectedItem that is set by the DETECTION_BASE derived classes, used to group the DetectedItem. - -- * A DetectedSet from the DetectedItems[] list can be retrieved using the method @{Detection#DETECTION_BASE.GetDetectedSet}( DetectedItemIndex ). - -- This method retrieves the Set from a DetectedItem element from the DetectedItem list (DetectedItems[ DetectedItemIndex ].Set ). - -- - -- ## **Visual filters** to fine-tune the probability of the detected objects - -- - -- By default, DCS World will return any object that is in LOS and within "visual reach", or detectable through one of the electronic detection means. - -- That being said, the DCS World detection algorithm can sometimes be unrealistic. - -- Especially for a visual detection, DCS World is able to report within 1 second a detailed detection of a group of 20 units (including types of the units) that are 10 kilometers away, using only visual capabilities. - -- Additionally, trees and other obstacles are not accounted during the DCS World detection. - -- - -- Therefore, an additional (optional) filtering has been built into the DETECTION_BASE class, that can be set for visual detected units. - -- For electronic detection, this filtering is not applied, only for visually detected targets. - -- - -- The following additional filtering can be applied for visual filtering: - -- - -- * A probability factor per kilometer distance. - -- * A probability factor based on the alpha angle between the detected object and the unit detecting. - -- A detection from a higher altitude allows for better detection than when on the ground. - -- * Define a probability factor for "cloudy zones", which are zones where forests or villages are located. In these zones, detection will be much more difficult. - -- The mission designer needs to define these cloudy zones within the mission, and needs to register these zones in the DETECTION_ objects additing a probability factor per zone. - -- - -- I advise however, that, when you first use the DETECTION derived classes, that you don't use these filters. - -- Only when you experience unrealistic behaviour in your missions, these filters could be applied. - -- - -- - -- ### Distance visual detection probability - -- - -- Upon a **visual** detection, the further away a detected object is, the less likely it is to be detected properly. - -- Also, the speed of accurate detection plays a role. - -- - -- A distance probability factor between 0 and 1 can be given, that will model a linear extrapolated probability over 10 km distance. - -- - -- For example, if a probability factor of 0.6 (60%) is given, the extrapolated probabilities over 15 kilometers would like like: - -- 1 km: 96%, 2 km: 92%, 3 km: 88%, 4 km: 84%, 5 km: 80%, 6 km: 76%, 7 km: 72%, 8 km: 68%, 9 km: 64%, 10 km: 60%, 11 km: 56%, 12 km: 52%, 13 km: 48%, 14 km: 44%, 15 km: 40%. - -- - -- Note that based on this probability factor, not only the detection but also the **type** of the unit will be applied! - -- - -- Use the method @{Detection#DETECTION_BASE.SetDistanceProbability}() to set the probability factor upon a 10 km distance. - -- - -- ### Alpha Angle visual detection probability - -- - -- Upon a **visual** detection, the higher the unit is during the detecting process, the more likely the detected unit is to be detected properly. - -- A detection at a 90% alpha angle is the most optimal, a detection at 10% is less and a detection at 0% is less likely to be correct. - -- - -- A probability factor between 0 and 1 can be given, that will model a progressive extrapolated probability if the target would be detected at a 0° angle. - -- - -- For example, if a alpha angle probability factor of 0.7 is given, the extrapolated probabilities of the different angles would look like: - -- 0°: 70%, 10°: 75,21%, 20°: 80,26%, 30°: 85%, 40°: 89,28%, 50°: 92,98%, 60°: 95,98%, 70°: 98,19%, 80°: 99,54%, 90°: 100% - -- - -- Use the method @{Detection#DETECTION_BASE.SetAlphaAngleProbability}() to set the probability factor if 0°. - -- - -- ### Cloudy Zones detection probability - -- - -- Upon a **visual** detection, the more a detected unit is within a cloudy zone, the less likely the detected unit is to be detected successfully. - -- The Cloudy Zones work with the ZONE_BASE derived classes. The mission designer can define within the mission - -- zones that reflect cloudy areas where detected units may not be so easily visually detected. - -- - -- Use the method @{Detection#DETECTION_BASE.SetZoneProbability}() to set for a defined number of zones, the probability factors. - -- - -- Note however, that the more zones are defined to be "cloudy" within a detection, the more performance it will take - -- from the DETECTION_BASE to calculate the presence of the detected unit within each zone. - -- Expecially for ZONE_POLYGON, try to limit the amount of nodes of the polygon! - -- - -- Typically, this kind of filter would be applied for very specific areas were a detection needs to be very realisting for - -- AI not to detect so easily targets within a forrest or village rich area. - -- - -- ## Accept / Reject detected units - -- - -- DETECTION_BASE can accept or reject successful detections based on the location of the detected object, - -- if it is located in range or located inside or outside of specific zones. - -- - -- ### Detection acceptance of within range limit - -- - -- A range can be set that will limit a successful detection for a unit. - -- Use the method @{Detection#DETECTION_BASE.SetAcceptRange}() to apply a range in meters till where detected units will be accepted. - -- - -- local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers. - -- - -- -- Build a detect object. - -- local Detection = DETECTION_UNITS:New( SetGroup ) - -- - -- -- This will accept detected units if the range is below 5000 meters. - -- Detection:SetAcceptRange( 5000 ) - -- - -- -- Start the Detection. - -- Detection:Start() - -- - -- - -- ### Detection acceptance if within zone(s). - -- - -- Specific ZONE_BASE object(s) can be given as a parameter, which will only accept a detection if the unit is within the specified ZONE_BASE object(s). - -- Use the method @{Detection#DETECTION_BASE.SetAcceptZones}() will accept detected units if they are within the specified zones. - -- - -- local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers. - -- - -- -- Search fo the zones where units are to be accepted. - -- local ZoneAccept1 = ZONE:New( "AcceptZone1" ) - -- local ZoneAccept2 = ZONE:New( "AcceptZone2" ) - -- - -- -- Build a detect object. - -- local Detection = DETECTION_UNITS:New( SetGroup ) - -- - -- -- This will accept detected units by Detection when the unit is within ZoneAccept1 OR ZoneAccept2. - -- Detection:SetAcceptZones( { ZoneAccept1, ZoneAccept2 } ) - -- - -- -- Start the Detection. - -- Detection:Start() - -- - -- ### Detection rejectance if within zone(s). - -- - -- Specific ZONE_BASE object(s) can be given as a parameter, which will reject detection if the unit is within the specified ZONE_BASE object(s). - -- Use the method @{Detection#DETECTION_BASE.SetRejectZones}() will reject detected units if they are within the specified zones. - -- An example of how to use the method is shown below. - -- - -- local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers. - -- - -- -- Search fo the zones where units are to be rejected. - -- local ZoneReject1 = ZONE:New( "RejectZone1" ) - -- local ZoneReject2 = ZONE:New( "RejectZone2" ) - -- - -- -- Build a detect object. - -- local Detection = DETECTION_UNITS:New( SetGroup ) - -- - -- -- This will reject detected units by Detection when the unit is within ZoneReject1 OR ZoneReject2. - -- Detection:SetRejectZones( { ZoneReject1, ZoneReject2 } ) - -- - -- -- Start the Detection. - -- Detection:Start() - -- - -- ## Detection of Friendlies Nearby - -- - -- Use the method @{Detection#DETECTION_BASE.SetFriendliesRange}() to set the range what will indicate when friendlies are nearby - -- a DetectedItem. The default range is 6000 meters. For air detections, it is advisory to use about 30.000 meters. - -- - -- ## DETECTION_BASE is a Finite State Machine - -- - -- Various Events and State Transitions can be tailored using DETECTION_BASE. - -- - -- ### DETECTION_BASE States - -- - -- * **Detecting**: The detection is running. - -- * **Stopped**: The detection is stopped. - -- - -- ### DETECTION_BASE Events - -- - -- * **Start**: Start the detection process. - -- * **Detect**: Detect new units. - -- * **Detected**: New units have been detected. - -- * **Stop**: Stop the detection process. - -- - -- @field #DETECTION_BASE DETECTION_BASE - -- - DETECTION_BASE = { - ClassName = "DETECTION_BASE", - DetectionSetGroup = nil, - DetectionRange = nil, - DetectedObjects = {}, - DetectionRun = 0, - DetectedObjectsIdentified = {}, - DetectedItems = {}, - } - - --- @type DETECTION_BASE.DetectedObjects - -- @list <#DETECTION_BASE.DetectedObject> - - --- @type DETECTION_BASE.DetectedObject - -- @field #string Name - -- @field #boolean IsVisible - -- @field #boolean KnowType - -- @field #boolean KnowDistance - -- @field #string Type - -- @field #number Distance - -- @field #boolean Identified - -- @field #number LastTime - -- @field #boolean LastPos - -- @field #number LastVelocity - - - --- @type DETECTION_BASE.DetectedItems - -- @list <#DETECTION_BASE.DetectedItem> - - --- @type DETECTION_BASE.DetectedItem - -- @field Core.Set#SET_UNIT Set - -- @field Core.Set#SET_UNIT Set -- The Set of Units in the detected area. - -- @field Core.Zone#ZONE_UNIT Zone -- The Zone of the detected area. - -- @field #boolean Changed Documents if the detected area has changes. - -- @field #table Changes A list of the changes reported on the detected area. (It is up to the user of the detected area to consume those changes). - -- @field #number ID -- The identifier of the detected area. - -- @field #boolean FriendliesNearBy Indicates if there are friendlies within the detected area. - -- @field Wrapper.Unit#UNIT NearestFAC The nearest FAC near the Area. - -- @field Core.Point#COORDINATE Coordinate The last known coordinate of the DetectedItem. - - --- DETECTION constructor. - -- @param #DETECTION_BASE self - -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. - -- @return #DETECTION_BASE self - function DETECTION_BASE:New( DetectionSetGroup ) - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM:New() ) -- #DETECTION_BASE - - self.DetectedItemCount = 0 - self.DetectedItemMax = 0 - self.DetectedItems = {} - - self.DetectionSetGroup = DetectionSetGroup - - self.RefreshTimeInterval = 30 - - self:InitDetectVisual( nil ) - self:InitDetectOptical( nil ) - self:InitDetectRadar( nil ) - self:InitDetectRWR( nil ) - self:InitDetectIRST( nil ) - self:InitDetectDLINK( nil ) - - self:FilterCategories( { - Unit.Category.AIRPLANE, - Unit.Category.GROUND_UNIT, - Unit.Category.HELICOPTER, - Unit.Category.SHIP, - Unit.Category.STRUCTURE - } ) - - self:SetFriendliesRange( 6000 ) - - -- Create FSM transitions. - - self:SetStartState( "Stopped" ) - - self:AddTransition( "Stopped", "Start", "Detecting") - - --- OnLeave Transition Handler for State Stopped. - -- @function [parent=#DETECTION_BASE] OnLeaveStopped - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnEnter Transition Handler for State Stopped. - -- @function [parent=#DETECTION_BASE] OnEnterStopped - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- OnBefore Transition Handler for Event Start. - -- @function [parent=#DETECTION_BASE] OnBeforeStart - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Start. - -- @function [parent=#DETECTION_BASE] OnAfterStart - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Start. - -- @function [parent=#DETECTION_BASE] Start - -- @param #DETECTION_BASE self - - --- Asynchronous Event Trigger for Event Start. - -- @function [parent=#DETECTION_BASE] __Start - -- @param #DETECTION_BASE self - -- @param #number Delay The delay in seconds. - - --- OnLeave Transition Handler for State Detecting. - -- @function [parent=#DETECTION_BASE] OnLeaveDetecting - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnEnter Transition Handler for State Detecting. - -- @function [parent=#DETECTION_BASE] OnEnterDetecting - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - self:AddTransition( "Detecting", "Detect", "Detecting" ) - self:AddTransition( "Detecting", "DetectionGroup", "Detecting" ) - - --- OnBefore Transition Handler for Event Detect. - -- @function [parent=#DETECTION_BASE] OnBeforeDetect - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Detect. - -- @function [parent=#DETECTION_BASE] OnAfterDetect - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Detect. - -- @function [parent=#DETECTION_BASE] Detect - -- @param #DETECTION_BASE self - - --- Asynchronous Event Trigger for Event Detect. - -- @function [parent=#DETECTION_BASE] __Detect - -- @param #DETECTION_BASE self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "Detecting", "Detected", "Detecting" ) - - --- OnBefore Transition Handler for Event Detected. - -- @function [parent=#DETECTION_BASE] OnBeforeDetected - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Detected. - -- @function [parent=#DETECTION_BASE] OnAfterDetected - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Detected. - -- @function [parent=#DETECTION_BASE] Detected - -- @param #DETECTION_BASE self - - --- Asynchronous Event Trigger for Event Detected. - -- @function [parent=#DETECTION_BASE] __Detected - -- @param #DETECTION_BASE self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "*", "Stop", "Stopped" ) - - --- OnBefore Transition Handler for Event Stop. - -- @function [parent=#DETECTION_BASE] OnBeforeStop - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Stop. - -- @function [parent=#DETECTION_BASE] OnAfterStop - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Stop. - -- @function [parent=#DETECTION_BASE] Stop - -- @param #DETECTION_BASE self - - --- Asynchronous Event Trigger for Event Stop. - -- @function [parent=#DETECTION_BASE] __Stop - -- @param #DETECTION_BASE self - -- @param #number Delay The delay in seconds. - - --- OnLeave Transition Handler for State Stopped. - -- @function [parent=#DETECTION_BASE] OnLeaveStopped - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnEnter Transition Handler for State Stopped. - -- @function [parent=#DETECTION_BASE] OnEnterStopped - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - return self - end - - do -- State Transition Handling - - --- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - function DETECTION_BASE:onafterStart(From,Event,To) - self:__Detect( 1 ) - end - - --- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - function DETECTION_BASE:onafterDetect(From,Event,To) - self:E( { From, Event, To } ) - - local DetectDelay = 0.1 - self.DetectionCount = 0 - self.DetectionRun = 0 - self:UnIdentifyAllDetectedObjects() -- Resets the DetectedObjectsIdentified table - - local DetectionTimeStamp = timer.getTime() - - for DetectionGroupID, DetectionGroupData in pairs( self.DetectionSetGroup:GetSet() ) do - --self:E( { DetectionGroupData } ) - self:__DetectionGroup( DetectDelay, DetectionGroupData, DetectionTimeStamp ) -- Process each detection asynchronously. - self.DetectionCount = self.DetectionCount + 1 - DetectDelay = DetectDelay + 1 - end - end - - --- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param Wrapper.Group#GROUP DetectionGroup The Group detecting. - function DETECTION_BASE:onafterDetectionGroup( From, Event, To, DetectionGroup, DetectionTimeStamp ) - self:E( { From, Event, To } ) - - self.DetectionRun = self.DetectionRun + 1 - - local HasDetectedObjects = false - - if DetectionGroup:IsAlive() then - - self:T( { "DetectionGroup is Alive", DetectionGroup:GetName() } ) - - local DetectionGroupName = DetectionGroup:GetName() - local DetectionUnit = DetectionGroup:GetUnit(1) - - local DetectedUnits = {} - - local DetectedTargets = DetectionGroup:GetDetectedTargets( - self.DetectVisual, - self.DetectOptical, - self.DetectRadar, - self.DetectIRST, - self.DetectRWR, - self.DetectDLINK - ) - - self:F( DetectedTargets ) - - for DetectionObjectID, Detection in pairs( DetectedTargets ) do - local DetectedObject = Detection.object -- Dcs.DCSWrapper.Object#Object - - if DetectedObject and DetectedObject:isExist() and DetectedObject.id_ < 50000000 then -- and ( DetectedObject:getCategory() == Object.Category.UNIT or DetectedObject:getCategory() == Object.Category.STATIC ) then - - local TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity = DetectionUnit:IsTargetDetected( - DetectedObject, - self.DetectVisual, - self.DetectOptical, - self.DetectRadar, - self.DetectIRST, - self.DetectRWR, - self.DetectDLINK - ) - - self:T2( { TargetIsDetected = TargetIsDetected, TargetIsVisible = TargetIsVisible, TargetLastTime = TargetLastTime, TargetKnowType = TargetKnowType, TargetKnowDistance = TargetKnowDistance, TargetLastPos = TargetLastPos, TargetLastVelocity = TargetLastVelocity } ) - - local DetectionAccepted = true - - local DetectedObjectName = DetectedObject:getName() - local DetectedObjectType = DetectedObject:getTypeName() - - local DetectedObjectVec3 = DetectedObject:getPoint() - local DetectedObjectVec2 = { x = DetectedObjectVec3.x, y = DetectedObjectVec3.z } - local DetectionGroupVec3 = DetectionGroup:GetVec3() - local DetectionGroupVec2 = { x = DetectionGroupVec3.x, y = DetectionGroupVec3.z } - - local Distance = ( ( DetectedObjectVec3.x - DetectionGroupVec3.x )^2 + - ( DetectedObjectVec3.y - DetectionGroupVec3.y )^2 + - ( DetectedObjectVec3.z - DetectionGroupVec3.z )^2 - ) ^ 0.5 / 1000 - - local DetectedUnitCategory = DetectedObject:getDesc().category - - self:F( { "Detected Target:", DetectionGroupName, DetectedObjectName, DetectedObjectType, Distance, DetectedUnitCategory } ) - - -- Calculate Acceptance - - DetectionAccepted = self._.FilterCategories[DetectedUnitCategory] ~= nil and DetectionAccepted or false - --- if Distance > 15000 then --- if DetectedUnitCategory == Unit.Category.GROUND_UNIT or DetectedUnitCategory == Unit.Category.SHIP then --- if DetectedObject:hasSensors( Unit.SensorType.RADAR, Unit.RadarType.AS ) == false then --- DetectionAccepted = false --- end --- end --- end - - if self.AcceptRange and Distance > self.AcceptRange then - DetectionAccepted = false - end - - if self.AcceptZones then - for AcceptZoneID, AcceptZone in pairs( self.AcceptZones ) do - local AcceptZone = AcceptZone -- Core.Zone#ZONE_BASE - if AcceptZone:IsVec2InZone( DetectedObjectVec2 ) == false then - DetectionAccepted = false - end - end - end - - if self.RejectZones then - for RejectZoneID, RejectZone in pairs( self.RejectZones ) do - local RejectZone = RejectZone -- Core.Zone#ZONE_BASE - if RejectZone:IsPointVec2InZone( DetectedObjectVec2 ) == true then - DetectionAccepted = false - end - end - end - - -- Calculate additional probabilities - - if not self.DetectedObjects[DetectedObjectName] and Detection.visible and self.DistanceProbability then - local DistanceFactor = Distance / 4 - local DistanceProbabilityReversed = ( 1 - self.DistanceProbability ) * DistanceFactor - local DistanceProbability = 1 - DistanceProbabilityReversed - DistanceProbability = DistanceProbability * 30 / 300 - local Probability = math.random() -- Selects a number between 0 and 1 - self:T( { Probability, DistanceProbability } ) - if Probability > DistanceProbability then - DetectionAccepted = false - end - end - - if not self.DetectedObjects[DetectedObjectName] and Detection.visible and self.AlphaAngleProbability then - local NormalVec2 = { x = DetectedObjectVec2.x - DetectionGroupVec2.x, y = DetectedObjectVec2.y - DetectionGroupVec2.y } - local AlphaAngle = math.atan2( NormalVec2.y, NormalVec2.x ) - local Sinus = math.sin( AlphaAngle ) - local AlphaAngleProbabilityReversed = ( 1 - self.AlphaAngleProbability ) * ( 1 - Sinus ) - local AlphaAngleProbability = 1 - AlphaAngleProbabilityReversed - - AlphaAngleProbability = AlphaAngleProbability * 30 / 300 - - local Probability = math.random() -- Selects a number between 0 and 1 - self:T( { Probability, AlphaAngleProbability } ) - if Probability > AlphaAngleProbability then - DetectionAccepted = false - end - - end - - if not self.DetectedObjects[DetectedObjectName] and Detection.visible and self.ZoneProbability then - - for ZoneDataID, ZoneData in pairs( self.ZoneProbability ) do - self:E({ZoneData}) - local ZoneObject = ZoneData[1] -- Core.Zone#ZONE_BASE - local ZoneProbability = ZoneData[2] -- #number - ZoneProbability = ZoneProbability * 30 / 300 - - if ZoneObject:IsPointVec2InZone( DetectedObjectVec2 ) == true then - local Probability = math.random() -- Selects a number between 0 and 1 - self:T( { Probability, ZoneProbability } ) - if Probability > ZoneProbability then - DetectionAccepted = false - break - end - end - end - end - - if DetectionAccepted then - - HasDetectedObjects = true - - self.DetectedObjects[DetectedObjectName] = self.DetectedObjects[DetectedObjectName] or {} - self.DetectedObjects[DetectedObjectName].Name = DetectedObjectName - self.DetectedObjects[DetectedObjectName].IsDetected = TargetIsDetected - self.DetectedObjects[DetectedObjectName].IsVisible = TargetIsVisible - self.DetectedObjects[DetectedObjectName].LastTime = TargetLastTime - self.DetectedObjects[DetectedObjectName].LastPos = TargetLastPos - self.DetectedObjects[DetectedObjectName].LastVelocity = TargetLastVelocity - self.DetectedObjects[DetectedObjectName].KnowType = TargetKnowType - self.DetectedObjects[DetectedObjectName].KnowDistance = Detection.distance -- TargetKnowDistance - self.DetectedObjects[DetectedObjectName].Distance = Distance - self.DetectedObjects[DetectedObjectName].DetectionTimeStamp = DetectionTimeStamp - - self:F( { DetectedObject = self.DetectedObjects[DetectedObjectName] } ) - - local DetectedUnit = UNIT:FindByName( DetectedObjectName ) - - DetectedUnits[DetectedObjectName] = DetectedUnit - else - -- if beyond the DetectionRange then nullify... - if self.DetectedObjects[DetectedObjectName] then - self.DetectedObjects[DetectedObjectName] = nil - end - end - end - - self:T2( self.DetectedObjects ) - end - - if HasDetectedObjects then - self:__Detected( 0.1, DetectedUnits ) - end - - end - - if self.DetectionCount > 0 and self.DetectionRun == self.DetectionCount then - self:T( "--> Create Detection Sets" ) - - -- First check if all DetectedObjects were detected. - -- This is important. When there are DetectedObjects in the list, but were not detected, - -- And these remain undetected for more than 60 seconds, then these DetectedObjects will be flagged as not Detected. - -- IsDetected = false! - -- This is used in A2A_TASK_DISPATCHER to initiate fighter sweeping! The TASK_A2A_INTERCEPT tasks will be replaced with TASK_A2A_SWEEP tasks. - for DetectedObjectName, DetectedObject in pairs( self.DetectedObjects ) do - if self.DetectedObjects[DetectedObjectName].IsDetected == true and self.DetectedObjects[DetectedObjectName].DetectionTimeStamp + 60 <= DetectionTimeStamp then - self.DetectedObjects[DetectedObjectName].IsDetected = false - end - end - - self:CreateDetectionItems() -- Polymorphic call to Create/Update the DetectionItems list for the DETECTION_ class grouping method. - for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do - self:UpdateDetectedItemDetection( DetectedItem ) - self:CleanDetectionItem( DetectedItem, DetectedItemID ) -- Any DetectionItem that has a Set with zero elements in it, must be removed from the DetectionItems list. - end - - self:__Detect( self.RefreshTimeInterval ) - end - - end - - - end - - do -- DetectionItems Creation - - -- Clean the DetectedItem table. - -- @param #DETECTION_BASE self - -- @return #DETECTION_BASE - function DETECTION_BASE:CleanDetectionItem( DetectedItem, DetectedItemID ) - self:F2() - - -- We clean all DetectedItems. - -- if there are any remaining DetectedItems with no Set Objects then the Item in the DetectedItems must be deleted. - - local DetectedSet = DetectedItem.Set - - if DetectedSet:Count() == 0 then - self:RemoveDetectedItem( DetectedItemID ) - end - - return self - end - - --- Forget a Unit from a DetectionItem - -- @param #DETECTION_BASE self - -- @param #string UnitName The UnitName that needs to be forgotten from the DetectionItem Sets. - -- @return #DETECTION_BASE - function DETECTION_BASE:ForgetDetectedUnit( UnitName ) - self:F2() - - local DetectedItems = self:GetDetectedItems() - - for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do - local DetectedSet = self:GetDetectedSet( DetectedItemIndex ) - if DetectedSet then - DetectedSet:RemoveUnitsByName( UnitName ) - end - end - - return self - end - - --- Make a DetectionSet table. This function will be overridden in the derived clsses. - -- @param #DETECTION_BASE self - -- @return #DETECTION_BASE - function DETECTION_BASE:CreateDetectionItems() - self:F2() - - self:E( "Error, in DETECTION_BASE class..." ) - return self - end - - - end - - do -- Initialization methods - - --- Detect Visual. - -- @param #DETECTION_BASE self - -- @param #boolean DetectVisual - -- @return #DETECTION_BASE self - function DETECTION_BASE:InitDetectVisual( DetectVisual ) - - self.DetectVisual = DetectVisual - - return self - end - - --- Detect Optical. - -- @param #DETECTION_BASE self - -- @param #boolean DetectOptical - -- @return #DETECTION_BASE self - function DETECTION_BASE:InitDetectOptical( DetectOptical ) - self:F2() - - self.DetectOptical = DetectOptical - - return self - end - - --- Detect Radar. - -- @param #DETECTION_BASE self - -- @param #boolean DetectRadar - -- @return #DETECTION_BASE self - function DETECTION_BASE:InitDetectRadar( DetectRadar ) - self:F2() - - self.DetectRadar = DetectRadar - - return self - end - - --- Detect IRST. - -- @param #DETECTION_BASE self - -- @param #boolean DetectIRST - -- @return #DETECTION_BASE self - function DETECTION_BASE:InitDetectIRST( DetectIRST ) - self:F2() - - self.DetectIRST = DetectIRST - - return self - end - - --- Detect RWR. - -- @param #DETECTION_BASE self - -- @param #boolean DetectRWR - -- @return #DETECTION_BASE self - function DETECTION_BASE:InitDetectRWR( DetectRWR ) - self:F2() - - self.DetectRWR = DetectRWR - - return self - end - - --- Detect DLINK. - -- @param #DETECTION_BASE self - -- @param #boolean DetectDLINK - -- @return #DETECTION_BASE self - function DETECTION_BASE:InitDetectDLINK( DetectDLINK ) - self:F2() - - self.DetectDLINK = DetectDLINK - - return self - end - - end - - do -- Filter methods - - --- Filter the detected units based on Unit.Category - -- The different values of Unit.Category can be: - -- - -- * Unit.Category.AIRPLANE - -- * Unit.Category.GROUND_UNIT - -- * Unit.Category.HELICOPTER - -- * Unit.Category.SHIP - -- * Unit.Category.STRUCTURE - -- - -- Multiple Unit.Category entries can be given as a table and then these will be evaluated as an OR expression. - -- - -- Example to filter a single category (Unit.Category.AIRPLANE). - -- - -- DetectionObject:FilterCategories( Unit.Category.AIRPLANE ) - -- - -- Example to filter multiple categories (Unit.Category.AIRPLANE, Unit.Category.HELICOPTER). Note the {}. - -- - -- DetectionObject:FilterCategories( { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) - -- - -- @param #DETECTION_BASE self - -- @param #list FilterCategories The Categories entries - -- @return #DETECTION_BASE self - function DETECTION_BASE:FilterCategories( FilterCategories ) - self:F2() - - self._.FilterCategories = {} - if type( FilterCategories ) == "table" then - for CategoryID, Category in pairs( FilterCategories ) do - self._.FilterCategories[Category] = Category - end - else - self._.FilterCategories[FilterCategories] = FilterCategories - end - return self - - end - - end - - do - - --- Set the detection interval time in seconds. - -- @param #DETECTION_BASE self - -- @param #number RefreshTimeInterval Interval in seconds. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetRefreshTimeInterval( RefreshTimeInterval ) - self:F2() - - self.RefreshTimeInterval = RefreshTimeInterval - - return self - end - - end - - do -- Friendlies Radius - - --- Set the radius in meters to validate if friendlies are nearby. - -- @param #DETECTION_BASE self - -- @param #number FriendliesRange Radius to use when checking if Friendlies are nearby. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetFriendliesRange( FriendliesRange ) --R2.2 Friendlies range - self:F2() - - self.FriendliesRange = FriendliesRange - - return self - end - - end - - do -- Intercept Point - - --- Set the parameters to calculate to optimal intercept point. - -- @param #DETECTION_BASE self - -- @param #boolean Intercept Intercept is true if an intercept point is calculated. Intercept is false if it is disabled. The default Intercept is false. - -- @param #number IntereptDelay If Intercept is true, then InterceptDelay is the average time it takes to get airplanes airborne. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetIntercept( Intercept, InterceptDelay ) - self:F2() - - self.Intercept = Intercept - self.InterceptDelay = InterceptDelay - - return self - end - - end - - do -- Accept / Reject detected units - - --- Accept detections if within a range in meters. - -- @param #DETECTION_BASE self - -- @param #number AcceptRange Accept a detection if the unit is within the AcceptRange in meters. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetAcceptRange( AcceptRange ) - self:F2() - - self.AcceptRange = AcceptRange - - return self - end - - --- Accept detections if within the specified zone(s). - -- @param #DETECTION_BASE self - -- @param Core.Zone#ZONE_BASE AcceptZones Can be a list or ZONE_BASE objects, or a single ZONE_BASE object. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetAcceptZones( AcceptZones ) - self:F2() - - if type( AcceptZones ) == "table" then - if AcceptZones.ClassName and AcceptZones:IsInstanceOf( ZONE_BASE ) then - self.AcceptZones = { AcceptZones } - else - self.AcceptZones = AcceptZones - end - else - self:E( { "AcceptZones must be a list of ZONE_BASE derived objects or one ZONE_BASE derived object", AcceptZones } ) - error() - end - - return self - end - - --- Reject detections if within the specified zone(s). - -- @param #DETECTION_BASE self - -- @param Core.Zone#ZONE_BASE RejectZones Can be a list or ZONE_BASE objects, or a single ZONE_BASE object. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetRejectZones( RejectZones ) - self:F2() - - if type( RejectZones ) == "table" then - if RejectZones.ClassName and RejectZones:IsInstanceOf( ZONE_BASE ) then - self.RejectZones = { RejectZones } - else - self.RejectZones = RejectZones - end - else - self:E( { "RejectZones must be a list of ZONE_BASE derived objects or one ZONE_BASE derived object", RejectZones } ) - error() - end - - return self - end - - end - - do -- Probability methods - - --- Upon a **visual** detection, the further away a detected object is, the less likely it is to be detected properly. - -- Also, the speed of accurate detection plays a role. - -- A distance probability factor between 0 and 1 can be given, that will model a linear extrapolated probability over 10 km distance. - -- For example, if a probability factor of 0.6 (60%) is given, the extrapolated probabilities over 15 kilometers would like like: - -- 1 km: 96%, 2 km: 92%, 3 km: 88%, 4 km: 84%, 5 km: 80%, 6 km: 76%, 7 km: 72%, 8 km: 68%, 9 km: 64%, 10 km: 60%, 11 km: 56%, 12 km: 52%, 13 km: 48%, 14 km: 44%, 15 km: 40%. - -- @param #DETECTION_BASE self - -- @param DistanceProbability The probability factor. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetDistanceProbability( DistanceProbability ) - self:F2() - - self.DistanceProbability = DistanceProbability - - return self - end - - - --- Upon a **visual** detection, the higher the unit is during the detecting process, the more likely the detected unit is to be detected properly. - -- A detection at a 90% alpha angle is the most optimal, a detection at 10% is less and a detection at 0% is less likely to be correct. - -- - -- A probability factor between 0 and 1 can be given, that will model a progressive extrapolated probability if the target would be detected at a 0° angle. - -- - -- For example, if a alpha angle probability factor of 0.7 is given, the extrapolated probabilities of the different angles would look like: - -- 0°: 70%, 10°: 75,21%, 20°: 80,26%, 30°: 85%, 40°: 89,28%, 50°: 92,98%, 60°: 95,98%, 70°: 98,19%, 80°: 99,54%, 90°: 100% - -- @param #DETECTION_BASE self - -- @param AlphaAngleProbability The probability factor. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetAlphaAngleProbability( AlphaAngleProbability ) - self:F2() - - self.AlphaAngleProbability = AlphaAngleProbability - - return self - end - - --- Upon a **visual** detection, the more a detected unit is within a cloudy zone, the less likely the detected unit is to be detected successfully. - -- The Cloudy Zones work with the ZONE_BASE derived classes. The mission designer can define within the mission - -- zones that reflect cloudy areas where detected units may not be so easily visually detected. - -- @param #DETECTION_BASE self - -- @param ZoneArray Aray of a The ZONE_BASE object and a ZoneProbability pair.. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetZoneProbability( ZoneArray ) - self:F2() - - self.ZoneProbability = ZoneArray - - return self - end - - - end - - do -- Change processing - - --- Accepts changes from the detected item. - -- @param #DETECTION_BASE self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - -- @return #DETECTION_BASE self - function DETECTION_BASE:AcceptChanges( DetectedItem ) - - DetectedItem.Changed = false - DetectedItem.Changes = {} - - return self - end - - --- Add a change to the detected zone. - -- @param #DETECTION_BASE self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - -- @param #string ChangeCode - -- @return #DETECTION_BASE self - function DETECTION_BASE:AddChangeItem( DetectedItem, ChangeCode, ItemUnitType ) - - DetectedItem.Changed = true - local ID = DetectedItem.ID - - DetectedItem.Changes = DetectedItem.Changes or {} - DetectedItem.Changes[ChangeCode] = DetectedItem.Changes[ChangeCode] or {} - DetectedItem.Changes[ChangeCode].ID = ID - DetectedItem.Changes[ChangeCode].ItemUnitType = ItemUnitType - - self:E( { "Change on Detection Item:", DetectedItem.ID, ChangeCode, ItemUnitType } ) - - return self - end - - - --- Add a change to the detected zone. - -- @param #DETECTION_BASE self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - -- @param #string ChangeCode - -- @param #string ChangeUnitType - -- @return #DETECTION_BASE self - function DETECTION_BASE:AddChangeUnit( DetectedItem, ChangeCode, ChangeUnitType ) - - DetectedItem.Changed = true - local ID = DetectedItem.ID - - DetectedItem.Changes = DetectedItem.Changes or {} - DetectedItem.Changes[ChangeCode] = DetectedItem.Changes[ChangeCode] or {} - DetectedItem.Changes[ChangeCode][ChangeUnitType] = DetectedItem.Changes[ChangeCode][ChangeUnitType] or 0 - DetectedItem.Changes[ChangeCode][ChangeUnitType] = DetectedItem.Changes[ChangeCode][ChangeUnitType] + 1 - DetectedItem.Changes[ChangeCode].ID = ID - - self:E( { "Change on Detection Item:", DetectedItem.ID, ChangeCode, ChangeUnitType } ) - - return self - end - - - end - - do -- Friendly calculations - - --- This will allow during friendly search any recce or detection unit to be also considered as a friendly. - -- By default, recce aren't considered friendly, because that would mean that a recce would be also an attacking friendly, - -- and this is wrong. - -- However, in a CAP situation, when the CAP is part of an EWR network, the CAP is also an attacker. - -- This, this method allows to register for a detection the CAP unit name prefixes to be considered CAP. - -- @param #DETECTION_BASE self - -- @param #string FriendlyPrefixes A string or a list of prefixes. - -- @return #DETECTION_BASE - function DETECTION_BASE:SetFriendlyPrefixes( FriendlyPrefixes ) - - self.FriendlyPrefixes = self.FriendlyPrefixes or {} - if type( FriendlyPrefixes ) ~= "table" then - FriendlyPrefixes = { FriendlyPrefixes } - end - for PrefixID, Prefix in pairs( FriendlyPrefixes ) do - self:F( { FriendlyPrefix = Prefix } ) - self.FriendlyPrefixes[Prefix] = Prefix - end - return self - end - - --- Returns if there are friendlies nearby the FAC units ... - -- @param #DETECTION_BASE self - -- @return #boolean true if there are friendlies nearby - function DETECTION_BASE:IsFriendliesNearBy( DetectedItem ) - - return DetectedItem.FriendliesNearBy ~= nil or false - end - - --- Returns friendly units nearby the FAC units ... - -- @param #DETECTION_BASE self - -- @return #map<#string,Wrapper.Unit#UNIT> The map of Friendly UNITs. - function DETECTION_BASE:GetFriendliesNearBy( DetectedItem ) - - return DetectedItem.FriendliesNearBy - end - - --- Filters friendly units by unit category. - -- @param #DETECTION_BASE self - -- @param FriendliesCategory - -- @return #DETECTION_BASE - function DETECTION_BASE:FilterFriendliesCategory( FriendliesCategory ) - self.FriendliesCategory = FriendliesCategory - return self - end - - --- Returns if there are friendlies nearby the intercept ... - -- @param #DETECTION_BASE self - -- @return #boolean trhe if there are friendlies near the intercept. - function DETECTION_BASE:IsFriendliesNearIntercept( DetectedItem ) - - return DetectedItem.FriendliesNearIntercept ~= nil or false - end - - --- Returns friendly units nearby the intercept point ... - -- @param #DETECTION_BASE self - -- @return #map<#string,Wrapper.Unit#UNIT> The map of Friendly UNITs. - function DETECTION_BASE:GetFriendliesNearIntercept( DetectedItem ) - - return DetectedItem.FriendliesNearIntercept - end - - --- Returns the distance used to identify friendlies near the deteted item ... - -- @param #DETECTION_BASE self - -- @return #number The distance. - function DETECTION_BASE:GetFriendliesDistance( DetectedItem ) - - return DetectedItem.FriendliesDistance - end - - --- Returns if there are friendlies nearby the FAC units ... - -- @param #DETECTION_BASE self - -- @return #boolean trhe if there are friendlies nearby - function DETECTION_BASE:IsPlayersNearBy( DetectedItem ) - - return DetectedItem.PlayersNearBy ~= nil - end - - --- Returns friendly units nearby the FAC units ... - -- @param #DETECTION_BASE self - -- @return #map<#string,Wrapper.Unit#UNIT> The map of Friendly UNITs. - function DETECTION_BASE:GetPlayersNearBy( DetectedItem ) - - return DetectedItem.PlayersNearBy - end - - --- Background worker function to determine if there are friendlies nearby ... - -- @param #DETECTION_BASE self - function DETECTION_BASE:ReportFriendliesNearBy( ReportGroupData ) - self:F2() - - local DetectedItem = ReportGroupData.DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem - local DetectedSet = ReportGroupData.DetectedItem.Set - local DetectedUnit = DetectedSet:GetFirst() -- Wrapper.Unit#UNIT - - DetectedItem.FriendliesNearBy = nil - - -- We need to ensure that the DetectedUnit is alive! - if DetectedUnit and DetectedUnit:IsAlive() then - - local DetectedUnitCoord = DetectedUnit:GetCoordinate() - local InterceptCoord = ReportGroupData.InterceptCoord or DetectedUnitCoord - - local SphereSearch = { - id = world.VolumeType.SPHERE, - params = { - point = InterceptCoord:GetVec3(), - radius = self.FriendliesRange, - } - - } - - --- @param Dcs.DCSWrapper.Unit#Unit FoundDCSUnit - -- @param Wrapper.Group#GROUP ReportGroup - -- @param Set#SET_GROUP ReportSetGroup - local FindNearByFriendlies = function( FoundDCSUnit, ReportGroupData ) - - local DetectedItem = ReportGroupData.DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem - local DetectedSet = ReportGroupData.DetectedItem.Set - local DetectedUnit = DetectedSet:GetFirst() -- Wrapper.Unit#UNIT - local DetectedUnitCoord = DetectedUnit:GetCoordinate() - local InterceptCoord = ReportGroupData.InterceptCoord or DetectedUnitCoord - local ReportSetGroup = ReportGroupData.ReportSetGroup - - local EnemyCoalition = DetectedUnit:GetCoalition() - - local FoundUnitCoalition = FoundDCSUnit:getCoalition() - local FoundUnitName = FoundDCSUnit:getName() - local FoundUnitGroupName = FoundDCSUnit:getGroup():getName() - local EnemyUnitName = DetectedUnit:GetName() - - local FoundUnitInReportSetGroup = ReportSetGroup:FindGroup( FoundUnitGroupName ) ~= nil - self:T( { "Friendlies search:", FoundUnitName, FoundUnitCoalition, EnemyUnitName, EnemyCoalition, FoundUnitInReportSetGroup } ) - - if FoundUnitInReportSetGroup == true then - -- If the recce was part of the friendlies found, then check if the recce is part of the allowed friendly unit prefixes. - for PrefixID, Prefix in pairs( self.FriendlyPrefixes or {} ) do - self:F( { "FriendlyPrefix:", Prefix } ) - -- In case a match is found (so a recce unit name is part of the friendly prefixes), then report that recce to be part of the friendlies. - -- This is important if CAP planes (so planes using their own radar) to be scanning for targets as part of the EWR network. - -- But CAP planes are also attackers, so they need to be considered friendlies too! - -- I chose to use prefixes because it is the fastest way to check. - if string.find( FoundUnitName, Prefix:gsub ("-", "%%-"), 1 ) then - FoundUnitInReportSetGroup = false - break - end - end - end - - self:F( { "Friendlies search:", FoundUnitName, FoundUnitCoalition, EnemyUnitName, EnemyCoalition, FoundUnitInReportSetGroup } ) - - if FoundUnitCoalition ~= EnemyCoalition and FoundUnitInReportSetGroup == false then - local FriendlyUnit = UNIT:Find( FoundDCSUnit ) - local FriendlyUnitName = FriendlyUnit:GetName() - local FriendlyUnitCategory = FriendlyUnit:GetDesc().category - self:T( { FriendlyUnitCategory = FriendlyUnitCategory, FriendliesCategory = self.FriendliesCategory } ) - - --if ( not self.FriendliesCategory ) or ( self.FriendliesCategory and ( self.FriendliesCategory == FriendlyUnitCategory ) ) then - DetectedItem.FriendliesNearBy = DetectedItem.FriendliesNearBy or {} - DetectedItem.FriendliesNearBy[FriendlyUnitName] = FriendlyUnit - local Distance = DetectedUnitCoord:Get2DDistance( FriendlyUnit:GetCoordinate() ) - DetectedItem.FriendliesDistance = DetectedItem.FriendliesDistance or {} - DetectedItem.FriendliesDistance[Distance] = FriendlyUnit - self:T( { FriendlyUnitName = FriendlyUnitName, Distance = Distance } ) - --end - return true - end - - return true - end - - world.searchObjects( Object.Category.UNIT, SphereSearch, FindNearByFriendlies, ReportGroupData ) - - DetectedItem.PlayersNearBy = nil - local DetectionZone = ZONE_UNIT:New( "DetectionPlayers", DetectedUnit, self.FriendliesRange ) - - _DATABASE:ForEachPlayer( - --- @param Wrapper.Unit#UNIT PlayerUnit - function( PlayerUnitName ) - local PlayerUnit = UNIT:FindByName( PlayerUnitName ) - - if PlayerUnit and PlayerUnit:IsInZone(DetectionZone) then - - local PlayerUnitCategory = PlayerUnit:GetDesc().category - - if ( not self.FriendliesCategory ) or ( self.FriendliesCategory and ( self.FriendliesCategory == PlayerUnitCategory ) ) then - - DetectedItem.FriendliesNearBy = DetectedItem.FriendliesNearBy or {} - local PlayerUnitName = PlayerUnit:GetName() - - DetectedItem.PlayersNearBy = DetectedItem.PlayersNearBy or {} - DetectedItem.PlayersNearBy[PlayerUnitName] = PlayerUnit - - DetectedItem.FriendliesNearBy = DetectedItem.FriendliesNearBy or {} - DetectedItem.FriendliesNearBy[PlayerUnitName] = PlayerUnit - - local Distance = DetectedUnitCoord:Get2DDistance( PlayerUnit:GetCoordinate() ) - DetectedItem.FriendliesDistance = DetectedItem.FriendliesDistance or {} - DetectedItem.FriendliesDistance[Distance] = PlayerUnit - - end - end - end - ) - end - end - - end - - --- Determines if a detected object has already been identified during detection processing. - -- @param #DETECTION_BASE self - -- @param #DETECTION_BASE.DetectedObject DetectedObject - -- @return #boolean true if already identified. - function DETECTION_BASE:IsDetectedObjectIdentified( DetectedObject ) - --self:F3( DetectedObject.Name ) - - local DetectedObjectName = DetectedObject.Name - if DetectedObjectName then - local DetectedObjectIdentified = self.DetectedObjectsIdentified[DetectedObjectName] == true - self:T3( DetectedObjectIdentified ) - return DetectedObjectIdentified - else - return nil - end - end - - --- Identifies a detected object during detection processing. - -- @param #DETECTION_BASE self - -- @param #DETECTION_BASE.DetectedObject DetectedObject - function DETECTION_BASE:IdentifyDetectedObject( DetectedObject ) - --self:F( { "Identified:", DetectedObject.Name } ) - - local DetectedObjectName = DetectedObject.Name - self.DetectedObjectsIdentified[DetectedObjectName] = true - end - - --- UnIdentify a detected object during detection processing. - -- @param #DETECTION_BASE self - -- @param #DETECTION_BASE.DetectedObject DetectedObject - function DETECTION_BASE:UnIdentifyDetectedObject( DetectedObject ) - - local DetectedObjectName = DetectedObject.Name - self.DetectedObjectsIdentified[DetectedObjectName] = false - end - - --- UnIdentify all detected objects during detection processing. - -- @param #DETECTION_BASE self - function DETECTION_BASE:UnIdentifyAllDetectedObjects() - - self.DetectedObjectsIdentified = {} -- Table will be garbage collected. - end - - --- Gets a detected object with a given name. - -- @param #DETECTION_BASE self - -- @param #string ObjectName - -- @return #DETECTION_BASE.DetectedObject - function DETECTION_BASE:GetDetectedObject( ObjectName ) - --self:F2( ObjectName ) - - if ObjectName then - local DetectedObject = self.DetectedObjects[ObjectName] - - if DetectedObject then - -- Only return detected objects that are alive! - local DetectedUnit = UNIT:FindByName( ObjectName ) - if DetectedUnit and DetectedUnit:IsAlive() then - if self:IsDetectedObjectIdentified( DetectedObject ) == false then - return DetectedObject - end - end - end - end - - return nil - end - - - --- Gets a detected unit type name, taking into account the detection results. - -- @param #DETECTION_BASE self - -- @param Wrapper.Unit#UNIT DetectedUnit - -- @return #string The type name - function DETECTION_BASE:GetDetectedUnitTypeName( DetectedUnit ) - --self:F2( ObjectName ) - - if DetectedUnit and DetectedUnit:IsAlive() then - local DetectedUnitName = DetectedUnit:GetName() - local DetectedObject = self.DetectedObjects[DetectedUnitName] - - if DetectedObject then - if DetectedObject.KnowType then - return DetectedUnit:GetTypeName() - else - return "Unknown" - end - else - return "Unknown" - end - else - return "Dead:" .. DetectedUnit:GetName() - end - - return "Undetected:" .. DetectedUnit:GetName() - end - - - --- Adds a new DetectedItem to the DetectedItems list. - -- The DetectedItem is a table and contains a SET_UNIT in the field Set. - -- @param #DETECTION_BASE self - -- @param ItemPrefix - -- @param #string DetectedItemIndex The index of the DetectedItem. - -- @param Core.Set#SET_UNIT Set (optional) The Set of Units to be added. - -- @return #DETECTION_BASE.DetectedItem - function DETECTION_BASE:AddDetectedItem( ItemPrefix, DetectedItemIndex, Set ) - - local DetectedItem = {} - self.DetectedItemCount = self.DetectedItemCount + 1 - self.DetectedItemMax = self.DetectedItemMax + 1 - - if DetectedItemIndex then - self.DetectedItems[DetectedItemIndex] = DetectedItem - else - self.DetectedItems[self.DetectedItemCount] = DetectedItem - end - - DetectedItem.Set = Set or SET_UNIT:New():FilterDeads():FilterCrashes() - DetectedItem.Index = DetectedItemIndex or self.DetectedItemCount - DetectedItem.ItemID = ItemPrefix .. "." .. self.DetectedItemMax - DetectedItem.ID = self.DetectedItemMax - DetectedItem.Removed = false - - return DetectedItem - end - - --- Adds a new DetectedItem to the DetectedItems list. - -- The DetectedItem is a table and contains a SET_UNIT in the field Set. - -- @param #DETECTION_BASE self - -- @param #string DetectedItemIndex The index of the DetectedItem. - -- @param Core.Set#SET_UNIT Set (optional) The Set of Units to be added. - -- @param Core.Zone#ZONE_UNIT Zone (optional) The Zone to be added where the Units are located. - -- @return #DETECTION_BASE.DetectedItem - function DETECTION_BASE:AddDetectedItemZone( DetectedItemIndex, Set, Zone ) - - local DetectedItem = self:AddDetectedItem( "AREA", DetectedItemIndex, Set ) - - DetectedItem.Zone = Zone - - return DetectedItem - end - - --- Removes an existing DetectedItem from the DetectedItems list. - -- The DetectedItem is a table and contains a SET_UNIT in the field Set. - -- @param #DETECTION_BASE self - -- @param #number DetectedItemIndex The index or position in the DetectedItems list where the item needs to be removed. - function DETECTION_BASE:RemoveDetectedItem( DetectedItemIndex ) - - if self.DetectedItems[DetectedItemIndex] then - self.DetectedItemCount = self.DetectedItemCount - 1 - self.DetectedItems[DetectedItemIndex] = nil - end - end - - - --- Get the detected @{Set#SET_BASE}s. - -- @param #DETECTION_BASE self - -- @return #DETECTION_BASE.DetectedItems - function DETECTION_BASE:GetDetectedItems() - - return self.DetectedItems - end - - --- Get the amount of SETs with detected objects. - -- @param #DETECTION_BASE self - -- @return #number The amount of detected items. Note that the amount of detected items can differ with the reality, because detections are not real-time but doen in intervals! - function DETECTION_BASE:GetDetectedItemsCount() - - local DetectedCount = self.DetectedItemCount - return DetectedCount - end - - --- Get a detected item using a given numeric index. - -- @param #DETECTION_BASE self - -- @param #number Index - -- @return #DETECTION_BASE.DetectedItem - function DETECTION_BASE:GetDetectedItem( Index ) - - local DetectedItem = self.DetectedItems[Index] - if DetectedItem then - return DetectedItem - end - - return nil - end - - --- Get a detected ItemID using a given numeric index. - -- @param #DETECTION_BASE self - -- @param #number Index - -- @return #string DetectedItemID - function DETECTION_BASE:GetDetectedItemID( Index ) --R2.1 - - local DetectedItem = self.DetectedItems[Index] - if DetectedItem then - return DetectedItem.ItemID - end - - return "" - end - - --- Get a detected ID using a given numeric index. - -- @param #DETECTION_BASE self - -- @param #number Index - -- @return #string DetectedItemID - function DETECTION_BASE:GetDetectedID( Index ) --R2.1 - - local DetectedItem = self.DetectedItems[Index] - if DetectedItem then - return DetectedItem.ID - end - - return "" - end - - --- Get the @{Set#SET_UNIT} of a detecttion area using a given numeric index. - -- @param #DETECTION_BASE self - -- @param #number Index - -- @return Core.Set#SET_UNIT DetectedSet - function DETECTION_BASE:GetDetectedSet( Index ) - - local DetectedItem = self:GetDetectedItem( Index ) - local DetectedSetUnit = DetectedItem.Set - if DetectedSetUnit then - return DetectedSetUnit - end - - return nil - end - - --- Set IsDetected flag for all DetectedItems. - -- @param #DETECTION_BASE self - -- @return #DETECTION_BASE.DetectedItem DetectedItem - -- @return #boolean true if at least one UNIT is detected from the DetectedSet, false if no UNIT was detected from the DetectedSet. - function DETECTION_BASE:UpdateDetectedItemDetection( DetectedItem ) - - local IsDetected = false - - for UnitName, UnitData in pairs( DetectedItem.Set:GetSet() ) do - local DetectedObject = self.DetectedObjects[UnitName] - self:F({UnitName = UnitName, IsDetected = DetectedObject.IsDetected}) - if DetectedObject.IsDetected then - IsDetected = true - break - end - end - - self:F( { IsDetected = DetectedItem.IsDetected } ) - - DetectedItem.IsDetected = IsDetected - - return IsDetected - end - - --- Checks if there is at least one UNIT detected in the Set of the the DetectedItem. - -- @param #DETECTION_BASE self - -- @return #boolean true if at least one UNIT is detected from the DetectedSet, false if no UNIT was detected from the DetectedSet. - function DETECTION_BASE:IsDetectedItemDetected( DetectedItem ) - - return DetectedItem.IsDetected - end - - - do -- Zones - - --- Get the @{Zone#ZONE_UNIT} of a detection area using a given numeric index. - -- @param #DETECTION_BASE self - -- @param #number Index - -- @return Core.Zone#ZONE_UNIT DetectedZone - function DETECTION_BASE:GetDetectedItemZone( Index ) - - local DetectedZone = self.DetectedItems[Index].Zone - if DetectedZone then - return DetectedZone - end - - local Detected - - return nil - end - - end - - - --- Set the detected item coordinate. - -- @param #DETECTION_BASE self - -- @param #DETECTION_BASE.DetectedItem The DetectedItem to set the coordinate at. - -- @param Core.Point#COORDINATE Coordinate The coordinate to set the last know detected position at. - -- @param Wrapper.Unit#UNIT DetectedItemUnit The unit to set the heading and altitude from. - -- @return #DETECTION_BASE - function DETECTION_BASE:SetDetectedItemCoordinate( DetectedItem, Coordinate, DetectedItemUnit ) - self:F( { Coordinate = Coordinate } ) - - if DetectedItem then - if DetectedItemUnit then - DetectedItem.Coordinate = Coordinate - DetectedItem.Coordinate:SetHeading( DetectedItemUnit:GetHeading() ) - DetectedItem.Coordinate.y = DetectedItemUnit:GetAltitude() - DetectedItem.Coordinate:SetVelocity( DetectedItemUnit:GetVelocityMPS() ) - end - end - end - - - --- Get the detected item coordinate. - -- @param #DETECTION_BASE self - -- @param #number Index - -- @return Core.Point#COORDINATE - function DETECTION_BASE:GetDetectedItemCoordinate( Index ) - self:F( { Index = Index } ) - - local DetectedItem = self:GetDetectedItem( Index ) - - if DetectedItem then - return DetectedItem.Coordinate - end - - return nil - end - - --- Set the detected item threatlevel. - -- @param #DETECTION_BASE self - -- @param #DETECTION_BASE.DetectedItem The DetectedItem to calculate the threatlevel for. - -- @return #DETECTION_BASE - function DETECTION_BASE:SetDetectedItemThreatLevel( DetectedItem ) - - local DetectedSet = DetectedItem.Set - - if DetectedItem then - DetectedItem.ThreatLevel, DetectedItem.ThreatText = DetectedSet:CalculateThreatLevelA2G() - end - end - - - - --- Get the detected item coordinate. - -- @param #DETECTION_BASE self - -- @param #number Index - -- @return #number ThreatLevel - function DETECTION_BASE:GetDetectedItemThreatLevel( Index ) - self:F( { Index = Index } ) - - local DetectedItem = self:GetDetectedItem( Index ) - - if DetectedItem then - return DetectedItem.ThreatLevel or 0, DetectedItem.ThreatText or "" - end - - return nil, "" - end - - - - - - - --- Menu of a detected item using a given numeric index. - -- @param #DETECTION_BASE self - -- @param Index - -- @return #string - function DETECTION_BASE:DetectedItemMenu( Index, AttackGroup ) - self:F( Index ) - return nil - end - - - --- Report summary of a detected item using a given numeric index. - -- @param #DETECTION_BASE self - -- @param Index - -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. - -- @param Core.Settings#SETTINGS Settings Message formatting settings to use. - -- @return Core.Report#REPORT - function DETECTION_BASE:DetectedItemReportSummary( Index, AttackGroup, Settings ) - self:F( Index ) - return nil - end - - --- Report detailed of a detectedion result. - -- @param #DETECTION_BASE self - -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. - -- @return #string - function DETECTION_BASE:DetectedReportDetailed( AttackGroup ) - self:F() - return nil - end - - --- Get the detection Groups. - -- @param #DETECTION_BASE self - -- @return Core.Set#SET_GROUP - function DETECTION_BASE:GetDetectionSetGroup() - - local DetectionSetGroup = self.DetectionSetGroup - return DetectionSetGroup - end - - - --- Schedule the DETECTION construction. - -- @param #DETECTION_BASE self - -- @param #number DelayTime The delay in seconds to wait the reporting. - -- @param #number RepeatInterval The repeat interval in seconds for the reporting to happen repeatedly. - -- @return #DETECTION_BASE self - function DETECTION_BASE:Schedule( DelayTime, RepeatInterval ) - self:F2() - - self.ScheduleDelayTime = DelayTime - self.ScheduleRepeatInterval = RepeatInterval - - self.DetectionScheduler = SCHEDULER:New( self, self._DetectionScheduler, { self, "Detection" }, DelayTime, RepeatInterval ) - return self - end - -end - -do -- DETECTION_UNITS - - --- # DETECTION_UNITS class, extends @{Detection#DETECTION_BASE} - -- - -- The DETECTION_UNITS class will detect units within the battle zone. - -- It will build a DetectedItems list filled with DetectedItems. Each DetectedItem will contain a field Set, which contains a @{Set#SET_UNIT} containing ONE @{UNIT} object reference. - -- Beware that when the amount of units detected is large, the DetectedItems list will be large also. - -- - -- @type DETECTION_UNITS - -- @field Dcs.DCSTypes#Distance DetectionRange The range till which targets are detected. - -- @extends #DETECTION_BASE - DETECTION_UNITS = { - ClassName = "DETECTION_UNITS", - DetectionRange = nil, - } - - --- DETECTION_UNITS constructor. - -- @param Functional.Detection#DETECTION_UNITS self - -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. - -- @return Functional.Detection#DETECTION_UNITS self - function DETECTION_UNITS:New( DetectionSetGroup ) - - -- Inherits from DETECTION_BASE - local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup ) ) -- #DETECTION_UNITS - - self._SmokeDetectedUnits = false - self._FlareDetectedUnits = false - self._SmokeDetectedZones = false - self._FlareDetectedZones = false - self._BoundDetectedZones = false - - return self - end - - --- Make text documenting the changes of the detected zone. - -- @param #DETECTION_UNITS self - -- @param #DETECTION_UNITS.DetectedItem DetectedItem - -- @return #string The Changes text - function DETECTION_UNITS:GetChangeText( DetectedItem ) - self:F( DetectedItem ) - - local MT = {} - - for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do - - if ChangeCode == "AU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = " New target(s) detected: " .. table.concat( MTUT, ", " ) .. "." - end - - if ChangeCode == "RU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = " Invisible or destroyed target(s): " .. table.concat( MTUT, ", " ) .. "." - end - - end - - return table.concat( MT, "\n" ) - - end - - - --- Create the DetectedItems list from the DetectedObjects table. - -- For each DetectedItem, a one field array is created containing the Unit detected. - -- @param #DETECTION_UNITS self - -- @return #DETECTION_UNITS self - function DETECTION_UNITS:CreateDetectionItems() - self:F2( #self.DetectedObjects ) - - -- Loop the current detected items, and check if each object still exists and is detected. - - for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do - - local DetectedItemSet = DetectedItem.Set -- Core.Set#SET_UNIT - - for DetectedUnitName, DetectedUnitData in pairs( DetectedItemSet:GetSet() ) do - local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT - - local DetectedObject = nil - --self:E( DetectedUnit ) - if DetectedUnit:IsAlive() then - --self:E(DetectedUnit:GetName()) - DetectedObject = self:GetDetectedObject( DetectedUnit:GetName() ) - end - if DetectedObject then - - -- Yes, the DetectedUnit is still detected or exists. Flag as identified. - self:IdentifyDetectedObject( DetectedObject ) - - -- Update the detection with the new data provided. - DetectedItem.TypeName = DetectedUnit:GetTypeName() - DetectedItem.CategoryName = DetectedUnit:GetCategoryName() - DetectedItem.Name = DetectedObject.Name - DetectedItem.IsVisible = DetectedObject.IsVisible - DetectedItem.LastTime = DetectedObject.LastTime - DetectedItem.LastPos = DetectedObject.LastPos - DetectedItem.LastVelocity = DetectedObject.LastVelocity - DetectedItem.KnowType = DetectedObject.KnowType - DetectedItem.KnowDistance = DetectedObject.KnowDistance - DetectedItem.Distance = DetectedObject.Distance - else - -- There was no DetectedObject, remove DetectedUnit from the Set. - self:AddChangeUnit( DetectedItem, "RU", DetectedUnitName ) - DetectedItemSet:Remove( DetectedUnitName ) - end - end - end - - - -- Now we need to loop through the unidentified detected units and add these... These are all new items. - for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do - - local DetectedObject = self:GetDetectedObject( DetectedUnitName ) - if DetectedObject then - self:T( { "Detected Unit #", DetectedUnitName } ) - - local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Wrapper.Unit#UNIT - - if DetectedUnit then - local DetectedTypeName = DetectedUnit:GetTypeName() - local DetectedItem = self:GetDetectedItem( DetectedUnitName ) - if not DetectedItem then - self:T( "Added new DetectedItem" ) - DetectedItem = self:AddDetectedItem( "UNIT", DetectedUnitName ) - DetectedItem.TypeName = DetectedUnit:GetTypeName() - DetectedItem.Name = DetectedObject.Name - DetectedItem.IsVisible = DetectedObject.IsVisible - DetectedItem.LastTime = DetectedObject.LastTime - DetectedItem.LastPos = DetectedObject.LastPos - DetectedItem.LastVelocity = DetectedObject.LastVelocity - DetectedItem.KnowType = DetectedObject.KnowType - DetectedItem.KnowDistance = DetectedObject.KnowDistance - DetectedItem.Distance = DetectedObject.Distance - end - - DetectedItem.Set:AddUnit( DetectedUnit ) - self:AddChangeUnit( DetectedItem, "AU", DetectedTypeName ) - end - end - end - - for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - - local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem - local DetectedSet = DetectedItem.Set - - -- Set the last known coordinate. - local DetectedFirstUnit = DetectedSet:GetFirst() - local DetectedFirstUnitCoord = DetectedFirstUnit:GetCoordinate() - self:SetDetectedItemCoordinate( DetectedItem, DetectedFirstUnitCoord, DetectedFirstUnit ) - - self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table - --self:NearestFAC( DetectedItem ) - - end - - end - - --- Menu of a DetectedItem using a given numeric index. - -- @param #DETECTION_UNITS self - -- @param Index - -- @return #string - function DETECTION_UNITS:DetectedItemMenu( Index, AttackGroup ) - self:F( Index ) - - local DetectedItem = self:GetDetectedItem( Index ) - local DetectedSet = self:GetDetectedSet( Index ) - local DetectedItemID = self:GetDetectedItemID( Index ) - - self:T( DetectedSet ) - if DetectedSet then - local ReportSummary = "" - local UnitDistanceText = "" - local UnitCategoryText = "" - - local DetectedItemCoordinate = self:GetDetectedItemCoordinate( Index ) - local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup ) - - ReportSummary = string.format( - "%s - %s", - DetectedItemID, - DetectedItemCoordText - ) - self:T( ReportSummary ) - - return ReportSummary - end - end - - --- Report summary of a DetectedItem using a given numeric index. - -- @param #DETECTION_UNITS self - -- @param Index - -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. - -- @param Core.Settings#SETTINGS Settings Message formatting settings to use. - -- @return Core.Report#REPORT The report of the detection items. - function DETECTION_UNITS:DetectedItemReportSummary( Index, AttackGroup, Settings ) - self:F( { Index, self.DetectedItems } ) - - local DetectedItem = self:GetDetectedItem( Index ) - local DetectedItemID = self:GetDetectedItemID( Index ) - - if DetectedItem then - local ReportSummary = "" - local UnitDistanceText = "" - local UnitCategoryText = "" - - if DetectedItem.KnowType then - local UnitCategoryName = DetectedItem.CategoryName - if UnitCategoryName then - UnitCategoryText = UnitCategoryName - end - if DetectedItem.TypeName then - UnitCategoryText = UnitCategoryText .. " (" .. DetectedItem.TypeName .. ")" - end - else - UnitCategoryText = "Unknown" - end - - if DetectedItem.KnowDistance then - if DetectedItem.IsVisible then - UnitDistanceText = " at " .. string.format( "%.2f", DetectedItem.Distance ) .. " km" - end - else - if DetectedItem.IsVisible then - UnitDistanceText = " at +/- " .. string.format( "%.0f", DetectedItem.Distance ) .. " km" - end - end - - --TODO: solve Index reference - local DetectedItemCoordinate = self:GetDetectedItemCoordinate( Index ) - local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings ) - - local ThreatLevelA2G = self:GetDetectedItemThreatLevel( Index ) - - local Report = REPORT:New() - Report:Add(DetectedItemID .. ", " .. DetectedItemCoordText) - Report:Add( string.format( "Threat: [%s]", string.rep( "■", ThreatLevelA2G ) ) ) - Report:Add( string.format("Type: %s%s", UnitCategoryText, UnitDistanceText ) ) - return Report - end - return nil - end - - - --- Report detailed of a detection result. - -- @param #DETECTION_UNITS self - -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. - -- @return #string - function DETECTION_UNITS:DetectedReportDetailed( AttackGroup ) - self:F() - - local Report = REPORT:New() - for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do - local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem - local ReportSummary = self:DetectedItemReportSummary( DetectedItemID, AttackGroup ) - Report:SetTitle( "Detected units:" ) - Report:Add( ReportSummary:Text() ) - end - - local ReportText = Report:Text() - - return ReportText - end - -end - -do -- DETECTION_TYPES - - --- # 3) DETECTION_TYPES class, extends @{Detection#DETECTION_BASE} - -- - -- The DETECTION_TYPES class will detect units within the battle zone. - -- It will build a DetectedItems[] list filled with DetectedItems, grouped by the type of units detected. - -- Each DetectedItem will contain a field Set, which contains a @{Set#SET_UNIT} containing ONE @{UNIT} object reference. - -- Beware that when the amount of different types detected is large, the DetectedItems[] list will be large also. - -- - -- @type DETECTION_TYPES - -- @extends #DETECTION_BASE - DETECTION_TYPES = { - ClassName = "DETECTION_TYPES", - DetectionRange = nil, - } - - --- DETECTION_TYPES constructor. - -- @param Functional.Detection#DETECTION_TYPES self - -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Recce role. - -- @return Functional.Detection#DETECTION_TYPES self - function DETECTION_TYPES:New( DetectionSetGroup ) - - -- Inherits from DETECTION_BASE - local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup ) ) -- #DETECTION_TYPES - - self._SmokeDetectedUnits = false - self._FlareDetectedUnits = false - self._SmokeDetectedZones = false - self._FlareDetectedZones = false - self._BoundDetectedZones = false - - return self - end - - --- Make text documenting the changes of the detected zone. - -- @param #DETECTION_TYPES self - -- @param #DETECTION_TYPES.DetectedItem DetectedItem - -- @return #string The Changes text - function DETECTION_TYPES:GetChangeText( DetectedItem ) - self:F( DetectedItem ) - - local MT = {} - - for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do - - if ChangeCode == "AU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = " New target(s) detected: " .. table.concat( MTUT, ", " ) .. "." - end - - if ChangeCode == "RU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = " Invisible or destroyed target(s): " .. table.concat( MTUT, ", " ) .. "." - end - - end - - return table.concat( MT, "\n" ) - - end - - - --- Create the DetectedItems list from the DetectedObjects table. - -- For each DetectedItem, a one field array is created containing the Unit detected. - -- @param #DETECTION_TYPES self - -- @return #DETECTION_TYPES self - function DETECTION_TYPES:CreateDetectionItems() - self:F2( #self.DetectedObjects ) - - -- Loop the current detected items, and check if each object still exists and is detected. - - for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do - - local DetectedItemSet = DetectedItem.Set -- Core.Set#SET_UNIT - local DetectedTypeName = DetectedItem.TypeName - - for DetectedUnitName, DetectedUnitData in pairs( DetectedItemSet:GetSet() ) do - local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT - - local DetectedObject = nil - if DetectedUnit:IsAlive() then - --self:E(DetectedUnit:GetName()) - DetectedObject = self:GetDetectedObject( DetectedUnit:GetName() ) - end - if DetectedObject then - - -- Yes, the DetectedUnit is still detected or exists. Flag as identified. - self:IdentifyDetectedObject( DetectedObject ) - else - -- There was no DetectedObject, remove DetectedUnit from the Set. - self:AddChangeUnit( DetectedItem, "RU", DetectedUnitName ) - DetectedItemSet:Remove( DetectedUnitName ) - end - end - end - - - -- Now we need to loop through the unidentified detected units and add these... These are all new items. - for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do - - local DetectedObject = self:GetDetectedObject( DetectedUnitName ) - if DetectedObject then - self:T( { "Detected Unit #", DetectedUnitName } ) - - local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Wrapper.Unit#UNIT - - if DetectedUnit then - local DetectedTypeName = DetectedUnit:GetTypeName() - local DetectedItem = self:GetDetectedItem( DetectedTypeName ) - if not DetectedItem then - DetectedItem = self:AddDetectedItem( "TYPE", DetectedTypeName ) - DetectedItem.TypeName = DetectedUnit:GetTypeName() - end - - DetectedItem.Set:AddUnit( DetectedUnit ) - self:AddChangeUnit( DetectedItem, "AU", DetectedTypeName ) - end - end - end - - - - -- Check if there are any friendlies nearby. - for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - - local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem - local DetectedSet = DetectedItem.Set - - -- Set the last known coordinate. - local DetectedFirstUnit = DetectedSet:GetFirst() - local DetectedUnitCoord = DetectedFirstUnit:GetCoordinate() - self:SetDetectedItemCoordinate( DetectedItem, DetectedUnitCoord, DetectedFirstUnit ) - - self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table - --self:NearestFAC( DetectedItem ) - end - - - - end - - --- Menu of a DetectedItem using a given numeric index. - -- @param #DETECTION_TYPES self - -- @param Index - -- @return #string - function DETECTION_TYPES:DetectedItemMenu( DetectedTypeName, AttackGroup ) - self:F( DetectedTypeName ) - - local DetectedItem = self:GetDetectedItem( DetectedTypeName ) - local DetectedItemID = self:GetDetectedItemID( DetectedTypeName ) - - if DetectedItem then - - local DetectedItemCoordinate = self:GetDetectedItemCoordinate( DetectedTypeName ) - local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup ) - - local ReportSummary = string.format( - "%s - %s", - DetectedItemID, - DetectedItemCoordText - ) - self:T( ReportSummary ) - - return ReportSummary - end - end - - --- Report summary of a DetectedItem using a given numeric index. - -- @param #DETECTION_TYPES self - -- @param Index - -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. - -- @param Core.Settings#SETTINGS Settings Message formatting settings to use. - -- @return Core.Report#REPORT The report of the detection items. - function DETECTION_TYPES:DetectedItemReportSummary( DetectedTypeName, AttackGroup, Settings ) - self:F( DetectedTypeName ) - - local DetectedItem = self:GetDetectedItem( DetectedTypeName ) - local DetectedSet = self:GetDetectedSet( DetectedTypeName ) - local DetectedItemID = self:GetDetectedItemID( DetectedTypeName ) - - self:T( DetectedItem ) - if DetectedItem then - - local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedTypeName ) - local DetectedItemsCount = DetectedSet:Count() - local DetectedItemType = DetectedItem.TypeName - - local DetectedItemCoordinate = self:GetDetectedItemCoordinate( DetectedTypeName ) - local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings ) - - local Report = REPORT:New() - Report:Add(DetectedItemID .. ", " .. DetectedItemCoordText) - Report:Add( string.format( "Threat: [%s]", string.rep( "■", ThreatLevelA2G ) ) ) - Report:Add( string.format("Type: %2d of %s", DetectedItemsCount, DetectedItemType ) ) - return Report - end - end - - --- Report detailed of a detection result. - -- @param #DETECTION_TYPES self - -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. - -- @return #string - function DETECTION_TYPES:DetectedReportDetailed( AttackGroup ) - self:F() - - local Report = REPORT:New() - for DetectedItemTypeName, DetectedItem in pairs( self.DetectedItems ) do - local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem - local ReportSummary = self:DetectedItemReportSummary( DetectedItemTypeName, AttackGroup ) - Report:SetTitle( "Detected types:" ) - Report:Add( ReportSummary:Text() ) - end - - local ReportText = Report:Text() - - return ReportText - end - -end - - -do -- DETECTION_AREAS - - --- # 4) DETECTION_AREAS class, extends @{Detection#DETECTION_BASE} - -- - -- The DETECTION_AREAS class will detect units within the battle zone for a list of @{Group}s detecting targets following (a) detection method(s), - -- and will build a list (table) of @{Set#SET_UNIT}s containing the @{Unit#UNIT}s detected. - -- The class is group the detected units within zones given a DetectedZoneRange parameter. - -- A set with multiple detected zones will be created as there are groups of units detected. - -- - -- ## 4.1) Retrieve the Detected Unit Sets and Detected Zones - -- - -- The methods to manage the DetectedItems[].Set(s) are implemented in @{Detection#DECTECTION_BASE} and - -- the methods to manage the DetectedItems[].Zone(s) is implemented in @{Detection#DETECTION_AREAS}. - -- - -- Retrieve the DetectedItems[].Set with the method @{Detection#DETECTION_BASE.GetDetectedSet}(). A @{Set#SET_UNIT} object will be returned. - -- - -- Retrieve the formed @{Zone@ZONE_UNIT}s as a result of the grouping the detected units within the DetectionZoneRange, use the method @{Detection#DETECTION_BASE.GetDetectionZones}(). - -- To understand the amount of zones created, use the method @{Detection#DETECTION_BASE.GetDetectionZoneCount}(). - -- If you want to obtain a specific zone from the DetectedZones, use the method @{Detection#DETECTION_BASE.GetDetectionZone}() with a given index. - -- - -- ## 4.4) Flare or Smoke detected units - -- - -- Use the methods @{Detection#DETECTION_AREAS.FlareDetectedUnits}() or @{Detection#DETECTION_AREAS.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place. - -- - -- ## 4.5) Flare or Smoke or Bound detected zones - -- - -- Use the methods: - -- - -- * @{Detection#DETECTION_AREAS.FlareDetectedZones}() to flare in a color - -- * @{Detection#DETECTION_AREAS.SmokeDetectedZones}() to smoke in a color - -- * @{Detection#DETECTION_AREAS.SmokeDetectedZones}() to bound with a tire with a white flag - -- - -- the detected zones when a new detection has taken place. - -- - -- @type DETECTION_AREAS - -- @field Dcs.DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. - -- @field #DETECTION_BASE.DetectedItems DetectedItems A list of areas containing the set of @{Unit}s, @{Zone}s, the center @{Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange. - -- @extends #DETECTION_BASE - DETECTION_AREAS = { - ClassName = "DETECTION_AREAS", - DetectionZoneRange = nil, - } - - - --- DETECTION_AREAS constructor. - -- @param #DETECTION_AREAS self - -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. - -- @param Dcs.DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. - -- @return #DETECTION_AREAS - function DETECTION_AREAS:New( DetectionSetGroup, DetectionZoneRange ) - - -- Inherits from DETECTION_BASE - local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup ) ) - - self.DetectionZoneRange = DetectionZoneRange - - self._SmokeDetectedUnits = false - self._FlareDetectedUnits = false - self._SmokeDetectedZones = false - self._FlareDetectedZones = false - self._BoundDetectedZones = false - - return self - end - - - --- Menu of a detected item using a given numeric index. - -- @param #DETECTION_AREAS self - -- @param Index - -- @return #string - function DETECTION_AREAS:DetectedItemMenu( Index, AttackGroup ) - self:F( Index ) - - local DetectedItem = self:GetDetectedItem( Index ) - local DetectedItemID = self:GetDetectedItemID( Index ) - - if DetectedItem then - local DetectedSet = self:GetDetectedSet( Index ) - local ReportSummaryItem - - local DetectedZone = self:GetDetectedItemZone( Index ) - local DetectedItemCoordinate = DetectedZone:GetCoordinate() - local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup ) - - local ReportSummary = string.format( - "%s - %s", - DetectedItemID, - DetectedItemCoordText - ) - - return ReportSummary - end - - return nil - end - - --- Report summary of a detected item using a given numeric index. - -- @param #DETECTION_AREAS self - -- @param Index - -- @param Wrapper.Group#GROUP AttackGroup The group to get the settings for. - -- @param Core.Settings#SETTINGS Settings (Optional) Message formatting settings to use. - -- @return Core.Report#REPORT The report of the detection items. - function DETECTION_AREAS:DetectedItemReportSummary( Index, AttackGroup, Settings ) - self:F( Index ) - - local DetectedItem = self:GetDetectedItem( Index ) - local DetectedItemID = self:GetDetectedItemID( Index ) - - if DetectedItem then - local DetectedSet = self:GetDetectedSet( Index ) - local ReportSummaryItem - - local DetectedZone = self:GetDetectedItemZone( Index ) - local DetectedItemCoordinate = DetectedZone:GetCoordinate() - local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings ) - - local ThreatLevelA2G = self:GetDetectedItemThreatLevel( Index ) - local DetectedItemsCount = DetectedSet:Count() - local DetectedItemsTypes = DetectedSet:GetTypeNames() - - local Report = REPORT:New() - Report:Add(DetectedItemID .. ", " .. DetectedItemCoordText) - Report:Add( string.format( "Threat: [%s]", string.rep( "■", ThreatLevelA2G ) ) ) - Report:Add( string.format("Type: %2d of %s", DetectedItemsCount, DetectedItemsTypes ) ) - - return Report - end - - return nil - end - - --- Report detailed of a detection result. - -- @param #DETECTION_AREAS self - -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. - -- @return #string - function DETECTION_AREAS:DetectedReportDetailed( AttackGroup ) --R2.1 Fixed missing report - self:F() - - local Report = REPORT:New() - for DetectedItemIndex, DetectedItem in pairs( self.DetectedItems ) do - local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem - local ReportSummary = self:DetectedItemReportSummary( DetectedItemIndex, AttackGroup ) - Report:SetTitle( "Detected areas:" ) - Report:Add( ReportSummary:Text() ) - end - - local ReportText = Report:Text() - - return ReportText - end - - - --- Calculate the optimal intercept point of the DetectedItem. - -- @param #DETECTION_AREAS self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - function DETECTION_AREAS:CalculateIntercept( DetectedItem ) - - local DetectedCoord = DetectedItem.Coordinate - local DetectedSpeed = DetectedCoord:GetVelocity() - local DetectedHeading = DetectedCoord:GetHeading() - - if self.Intercept then - local DetectedSet = DetectedItem.Set - -- todo: speed - - local TranslateDistance = DetectedSpeed * self.InterceptDelay - - local InterceptCoord = DetectedCoord:Translate( TranslateDistance, DetectedHeading ) - - DetectedItem.InterceptCoord = InterceptCoord - else - DetectedItem.InterceptCoord = DetectedCoord - end - - end - - - --- Find the nearest FAC of the DetectedItem. - -- @param #DETECTION_AREAS self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - -- @return Wrapper.Unit#UNIT The nearest FAC unit - function DETECTION_AREAS:NearestFAC( DetectedItem ) - - local NearestRecce = nil - local DistanceRecce = 1000000000 -- Units are not further than 1000000 km away from an area :-) - - for RecceGroupName, RecceGroup in pairs( self.DetectionSetGroup:GetSet() ) do - if RecceGroup and RecceGroup:IsAlive() then - for RecceUnit, RecceUnit in pairs( RecceGroup:GetUnits() ) do - if RecceUnit:IsActive() then - local RecceUnitCoord = RecceUnit:GetCoordinate() - local Distance = RecceUnitCoord:Get2DDistance( self:GetDetectedItemCoordinate( DetectedItem.Index ) ) - if Distance < DistanceRecce then - DistanceRecce = Distance - NearestRecce = RecceUnit - end - end - end - end - end - - DetectedItem.NearestFAC = NearestRecce - DetectedItem.DistanceRecce = DistanceRecce - - end - - --- Smoke the detected units - -- @param #DETECTION_AREAS self - -- @return #DETECTION_AREAS self - function DETECTION_AREAS:SmokeDetectedUnits() - self:F2() - - self._SmokeDetectedUnits = true - return self - end - - --- Flare the detected units - -- @param #DETECTION_AREAS self - -- @return #DETECTION_AREAS self - function DETECTION_AREAS:FlareDetectedUnits() - self:F2() - - self._FlareDetectedUnits = true - return self - end - - --- Smoke the detected zones - -- @param #DETECTION_AREAS self - -- @return #DETECTION_AREAS self - function DETECTION_AREAS:SmokeDetectedZones() - self:F2() - - self._SmokeDetectedZones = true - return self - end - - --- Flare the detected zones - -- @param #DETECTION_AREAS self - -- @return #DETECTION_AREAS self - function DETECTION_AREAS:FlareDetectedZones() - self:F2() - - self._FlareDetectedZones = true - return self - end - - --- Bound the detected zones - -- @param #DETECTION_AREAS self - -- @return #DETECTION_AREAS self - function DETECTION_AREAS:BoundDetectedZones() - self:F2() - - self._BoundDetectedZones = true - return self - end - - --- Make text documenting the changes of the detected zone. - -- @param #DETECTION_AREAS self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - -- @return #string The Changes text - function DETECTION_AREAS:GetChangeText( DetectedItem ) - self:F( DetectedItem ) - - local MT = {} - - for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do - - if ChangeCode == "AA" then - MT[#MT+1] = "Detected new area " .. ChangeData.ID .. ". The center target is a " .. ChangeData.ItemUnitType .. "." - end - - if ChangeCode == "RAU" then - MT[#MT+1] = "Changed area " .. ChangeData.ID .. ". Removed the center target." - end - - if ChangeCode == "AAU" then - MT[#MT+1] = "Changed area " .. ChangeData.ID .. ". The new center target is a " .. ChangeData.ItemUnitType .. "." - end - - if ChangeCode == "RA" then - MT[#MT+1] = "Removed old area " .. ChangeData.ID .. ". No more targets in this area." - end - - if ChangeCode == "AU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = "Detected for area " .. ChangeData.ID .. " new target(s) " .. table.concat( MTUT, ", " ) .. "." - end - - if ChangeCode == "RU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = "Removed for area " .. ChangeData.ID .. " invisible or destroyed target(s) " .. table.concat( MTUT, ", " ) .. "." - end - - end - - return table.concat( MT, "\n" ) - - end - - - --- Make a DetectionSet table. This function will be overridden in the derived clsses. - -- @param #DETECTION_AREAS self - -- @return #DETECTION_AREAS self - function DETECTION_AREAS:CreateDetectionItems() - self:F2() - - - self:T( "Checking Detected Items for new Detected Units ..." ) - -- First go through all detected sets, and check if there are new detected units, match all existing detected units and identify undetected units. - -- Regroup when needed, split groups when needed. - for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - - local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem - - if DetectedItem then - - self:T( { "Detected Item ID:", DetectedItemID } ) - - - local DetectedSet = DetectedItem.Set - - local AreaExists = false -- This flag will determine of the detected area is still existing. - - -- First test if the center unit is detected in the detection area. - self:T3( { "Zone Center Unit:", DetectedItem.Zone.ZoneUNIT.UnitName } ) - local DetectedZoneObject = self:GetDetectedObject( DetectedItem.Zone.ZoneUNIT.UnitName ) - self:T3( { "Detected Zone Object:", DetectedItem.Zone:GetName(), DetectedZoneObject } ) - - if DetectedZoneObject then - - --self:IdentifyDetectedObject( DetectedZoneObject ) - AreaExists = true - - - - else - -- The center object of the detected area has not been detected. Find an other unit of the set to become the center of the area. - -- First remove the center unit from the set. - DetectedSet:RemoveUnitsByName( DetectedItem.Zone.ZoneUNIT.UnitName ) - - self:AddChangeItem( DetectedItem, 'RAU', self:GetDetectedUnitTypeName( DetectedItem.Zone.ZoneUNIT ) ) - - -- Then search for a new center area unit within the set. Note that the new area unit candidate must be within the area range. - for DetectedUnitName, DetectedUnitData in pairs( DetectedSet:GetSet() ) do - - local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT - local DetectedObject = self:GetDetectedObject( DetectedUnit.UnitName ) - local DetectedUnitTypeName = self:GetDetectedUnitTypeName( DetectedUnit ) - - -- The DetectedObject can be nil when the DetectedUnit is not alive anymore or it is not in the DetectedObjects map. - -- If the DetectedUnit was already identified, DetectedObject will be nil. - if DetectedObject then - self:IdentifyDetectedObject( DetectedObject ) - AreaExists = true - - --DetectedItem.Zone:BoundZone( 12, self.CountryID, true) - - -- Assign the Unit as the new center unit of the detected area. - DetectedItem.Zone = ZONE_UNIT:New( DetectedUnit:GetName(), DetectedUnit, self.DetectionZoneRange ) - - self:AddChangeItem( DetectedItem, "AAU", DetectedUnitTypeName ) - - -- We don't need to add the DetectedObject to the area set, because it is already there ... - break - else - DetectedSet:Remove( DetectedUnitName ) - self:AddChangeUnit( DetectedItem, "RU", DetectedUnitTypeName ) - end - end - end - - -- Now we've determined the center unit of the area, now we can iterate the units in the detected area. - -- Note that the position of the area may have moved due to the center unit repositioning. - -- If no center unit was identified, then the detected area does not exist anymore and should be deleted, as there are no valid units that can be the center unit. - if AreaExists then - - -- ok, we found the center unit of the area, now iterate through the detected area set and see which units are still within the center unit zone ... - -- Those units within the zone are flagged as Identified. - -- If a unit was not found in the set, remove it from the set. This may be added later to other existing or new sets. - for DetectedUnitName, DetectedUnitData in pairs( DetectedSet:GetSet() ) do - - local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT - local DetectedUnitTypeName = self:GetDetectedUnitTypeName( DetectedUnit ) - - local DetectedObject = nil - if DetectedUnit:IsAlive() then - --self:E(DetectedUnit:GetName()) - DetectedObject = self:GetDetectedObject( DetectedUnit:GetName() ) - end - if DetectedObject then - - -- Check if the DetectedUnit is within the DetectedItem.Zone - if DetectedUnit:IsInZone( DetectedItem.Zone ) then - - -- Yes, the DetectedUnit is within the DetectedItem.Zone, no changes, DetectedUnit can be kept within the Set. - self:IdentifyDetectedObject( DetectedObject ) - - else - -- No, the DetectedUnit is not within the DetectedItem.Zone, remove DetectedUnit from the Set. - DetectedSet:Remove( DetectedUnitName ) - self:AddChangeUnit( DetectedItem, "RU", DetectedUnitTypeName ) - end - - else - -- There was no DetectedObject, remove DetectedUnit from the Set. - self:AddChangeUnit( DetectedItem, "RU", "destroyed target" ) - DetectedSet:Remove( DetectedUnitName ) - - -- The DetectedObject has been identified, because it does not exist ... - -- self:IdentifyDetectedObject( DetectedObject ) - end - end - else - --DetectedItem.Zone:BoundZone( 12, self.CountryID, true) - self:RemoveDetectedItem( DetectedItemID ) - self:AddChangeItem( DetectedItem, "RA" ) - end - end - end - - - - -- We iterated through the existing detection areas and: - -- - We checked which units are still detected in each detection area. Those units were flagged as Identified. - -- - We recentered the detection area to new center units where it was needed. - -- - -- Now we need to loop through the unidentified detected units and see where they belong: - -- - They can be added to a new detection area and become the new center unit. - -- - They can be added to a new detection area. - for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do - - local DetectedObject = self:GetDetectedObject( DetectedUnitName ) - - if DetectedObject then - - -- We found an unidentified unit outside of any existing detection area. - local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Wrapper.Unit#UNIT - local DetectedUnitTypeName = self:GetDetectedUnitTypeName( DetectedUnit ) - - local AddedToDetectionArea = false - - for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - - local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem - if DetectedItem then - self:T( "Detection Area #" .. DetectedItem.ID ) - local DetectedSet = DetectedItem.Set - if not self:IsDetectedObjectIdentified( DetectedObject ) and DetectedUnit:IsInZone( DetectedItem.Zone ) then - self:IdentifyDetectedObject( DetectedObject ) - DetectedSet:AddUnit( DetectedUnit ) - AddedToDetectionArea = true - self:AddChangeUnit( DetectedItem, "AU", DetectedUnitTypeName ) - end - end - end - - if AddedToDetectionArea == false then - - -- New detection area - local DetectedItem = self:AddDetectedItemZone( nil, - SET_UNIT:New():FilterDeads():FilterCrashes(), - ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange ) - ) - --self:E( DetectedItem.Zone.ZoneUNIT.UnitName ) - DetectedItem.Set:AddUnit( DetectedUnit ) - self:AddChangeItem( DetectedItem, "AA", DetectedUnitTypeName ) - end - end - end - - -- Now all the tests should have been build, now make some smoke and flares... - -- We also report here the friendlies within the detected areas. - - for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - - local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem - local DetectedSet = DetectedItem.Set - local DetectedFirstUnit = DetectedSet:GetFirst() - local DetectedZone = DetectedItem.Zone - - -- Set the last known coordinate to the detection item. - local DetectedZoneCoord = DetectedZone:GetCoordinate() - self:SetDetectedItemCoordinate( DetectedItem, DetectedZoneCoord, DetectedFirstUnit ) - - self:CalculateIntercept( DetectedItem ) - - self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table - self:SetDetectedItemThreatLevel( DetectedItem ) -- Calculate A2G threat level - self:NearestFAC( DetectedItem ) - - - if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then - DetectedZone.ZoneUNIT:SmokeRed() - end - - --DetectedSet:Flush() - - DetectedSet:ForEachUnit( - --- @param Wrapper.Unit#UNIT DetectedUnit - function( DetectedUnit ) - if DetectedUnit:IsAlive() then - --self:T( "Detected Set #" .. DetectedItem.ID .. ":" .. DetectedUnit:GetName() ) - if DETECTION_AREAS._FlareDetectedUnits or self._FlareDetectedUnits then - DetectedUnit:FlareGreen() - end - if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then - DetectedUnit:SmokeGreen() - end - end - end - ) - if DETECTION_AREAS._FlareDetectedZones or self._FlareDetectedZones then - DetectedZone:FlareZone( SMOKECOLOR.White, 30, math.random( 0,90 ) ) - end - if DETECTION_AREAS._SmokeDetectedZones or self._SmokeDetectedZones then - DetectedZone:SmokeZone( SMOKECOLOR.White, 30 ) - end - - if DETECTION_AREAS._BoundDetectedZones or self._BoundDetectedZones then - self.CountryID = DetectedSet:GetFirst():GetCountry() - DetectedZone:BoundZone( 12, self.CountryID ) - end - end - - end - -end ---- **Functional** -- Management of target **Designation**. Lase, smoke and illuminate targets. --- --- --![Banner Image](..\Presentations\DESIGNATE\Dia1.JPG) --- --- === --- --- DESIGNATE is orchestrating the designation of potential targets executed by a Recce group, --- and communicates these to a dedicated attacking group of players, --- so that following a dynamically generated menu system, --- each detected set of potential targets can be lased or smoked... --- --- Targets can be: --- --- * **Lased** for a period of time. --- * **Smoked**. Artillery or airplanes with Illuminatino ordonance need to be present. (WIP, but early demo ready.) --- * **Illuminated** through an illumination bomb. Artillery or airplanes with Illuminatino ordonance need to be present. (WIP, but early demo ready. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * [**Ciribob**](https://forums.eagle.ru/member.php?u=112175): Showing the way how to lase targets + how laser codes work!!! Explained the autolase script. --- * [**EasyEB**](https://forums.eagle.ru/member.php?u=112055): Ideas and Beta Testing --- * [**Wingthor**](https://forums.eagle.ru/member.php?u=123698): Beta Testing --- --- --- ### Authors: --- --- * **FlightControl**: Design & Programming --- --- @module Designate - - -do -- DESIGNATE - - --- @type DESIGNATE - -- @extends Core.Fsm#FSM_PROCESS - - --- # DESIGNATE class, extends @{Fsm#FSM} - -- - -- DESIGNATE is orchestrating the designation of potential targets executed by a Recce group, - -- and communicates these to a dedicated attacking group of players, - -- so that following a dynamically generated menu system, - -- each detected set of potential targets can be lased or smoked... - -- - -- Targets can be: - -- - -- * **Lased** for a period of time. - -- * **Smoked**. Artillery or airplanes with Illuminatino ordonance need to be present. (WIP, but early demo ready.) - -- * **Illuminated** through an illumination bomb. Artillery or airplanes with Illuminatino ordonance need to be present. (WIP, but early demo ready. - -- - -- The following terminology is being used throughout this document: - -- - -- * The **DesignateObject** is the object of the DESIGNATE class, which is this class explained in the document. - -- * The **DetectionObject** is the object of a DETECTION_ class (DETECTION_TYPES, DETECTION_AREAS, DETECTION_UNITS), which is executing the detection and grouping of Targets into _DetectionItems_. - -- * **DetectionItems** is the list of detected target groupings by the _DetectionObject_. Each _DetectionItem_ contains a _TargetSet_. - -- * **DetectionItem** is one element of the _DetectionItems_ list, and contains a _TargetSet_. - -- * The **TargetSet** is a SET_UNITS collection of _Targets_, that have been detected by the _DetectionObject_. - -- * A **Target** is a detected UNIT object by the _DetectionObject_. - -- * A **Threat Level** is a number from 0 to 10 that is calculated based on the threat of the Target in an Air to Ground battle scenario. - -- * The **RecceSet** is a SET_GROUP collection that contains the **RecceGroups**. - -- * A **RecceGroup** is a GROUP object containing the **Recces**. - -- * A **Recce** is a UNIT object executing the reconnaissance as part the _DetectionObject_. A Recce can be of any UNIT type. - -- * An **AttackGroup** is a GROUP object that contain _Players_. - -- * A **Player** is an active CLIENT object containing a human player. - -- * A **Designate Menu** is the menu that is dynamically created during the designation process for each _AttackGroup_. - -- - -- The RecceSet is continuously detecting for potential Targets, executing its task as part of the DetectionObject. - -- Once Targets have been detected, the DesignateObject will trigger the **Detect Event**. - -- - -- In order to prevent an overflow in the DesignateObject of detected targets, there is a maximum - -- amount of DetectionItems that can be put in **scope** of the DesignateObject. - -- We call this the **MaximumDesignations** term. - -- - -- As part of the Detect Event, the DetectionItems list is used by the DesignateObject to provide the Players with: - -- - -- * The RecceGroups are reporting to each AttackGroup, sending **Messages** containing the Threat Level and the TargetSet composition. - -- * **Menu options** are created and updated for each AttackGroup, containing the Detection ID and the Coordinates. - -- - -- A Player can then select an action from the Designate Menu. - -- - -- **Note that each selected action will be executed for a TargetSet, thus the Target grouping done by the DetectionObject.** - -- - -- Each **Menu Option** in the Designate Menu has two modes: - -- - -- 1. If the TargetSet **is not being designated**, then the **Designate Menu** option for the target Set will provide options to **Lase** or **Smoke** the targets. - -- 2. If the Target Set **is being designated**, then the **Designate Menu** option will provide an option to stop or cancel the designation. - -- - -- While designating, the RecceGroups will report any change in TargetSet composition or Target presence. - -- - -- The following logic is executed when a TargetSet is selected to be *lased* from the Designation Menu: - -- - -- * The RecceSet is searched for any Recce that is within *designation distance* from a Target in the TargetSet that is currently not being designated. - -- * If there is a Recce found that is currently no designating a target, and is within designation distance from the Target, then that Target will be designated. - -- * During designation, any Recce that does not have Line of Sight (LOS) and is not within disignation distance from the Target, will stop designating the Target, and a report is given. - -- * When a Recce is designating a Target, and that Target is destroyed, then the Recce will stop designating the Target, and will report the event. - -- * When a Recce is designating a Target, and that Recce is destroyed, then the Recce will be removed from the RecceSet and designation will stop without reporting. - -- * When all RecceGroups are destroyed from the RecceSet, then the DesignationObject will stop functioning, and nothing will be reported. - -- - -- In this way, the DesignationObject assists players to designate ground targets for a coordinated attack! - -- - -- Have FUN! - -- - -- ## 1. DESIGNATE constructor - -- - -- * @{#DESIGNATE.New}(): Creates a new DESIGNATE object. - -- - -- ## 2. DESIGNATE is a FSM - -- - -- ![Process](..\Presentations\DESIGNATE\Dia2.JPG) - -- - -- ### 2.1 DESIGNATE States - -- - -- * **Designating** ( Group ): The designation process. - -- - -- ### 2.2 DESIGNATE Events - -- - -- * **@{#DESIGNATE.Detect}**: Detect targets. - -- * **@{#DESIGNATE.LaseOn}**: Lase the targets with the specified Index. - -- * **@{#DESIGNATE.LaseOff}**: Stop lasing the targets with the specified Index. - -- * **@{#DESIGNATE.Smoke}**: Smoke the targets with the specified Index. - -- * **@{#DESIGNATE.Status}**: Report designation status. - -- - -- ## 3. Maximum Designations - -- - -- In order to prevent an overflow of designations due to many Detected Targets, there is a - -- Maximum Designations scope that is set in the DesignationObject. - -- - -- The method @{#DESIGNATE.SetMaximumDesignations}() will put a limit on the amount of designations put in scope of the DesignationObject. - -- Using the menu system, the player can "forget" a designation, so that gradually a new designation can be put in scope when detected. - -- - -- ## 4. Laser codes - -- - -- ### 4.1. Set possible laser codes - -- - -- An array of laser codes can be provided, that will be used by the DESIGNATE when lasing. - -- The laser code is communicated by the Recce when it is lasing a larget. - -- Note that the default laser code is 1113. - -- Working known laser codes are: 1113,1462,1483,1537,1362,1214,1131,1182,1644,1614,1515,1411,1621,1138,1542,1678,1573,1314,1643,1257,1467,1375,1341,1275,1237 - -- - -- Use the method @{#DESIGNATE.SetLaserCodes}() to set the possible laser codes to be selected from. - -- One laser code can be given or an sequence of laser codes through an table... - -- - -- Designate:SetLaserCodes( 1214 ) - -- - -- The above sets one laser code with the value 1214. - -- - -- Designate:SetLaserCodes( { 1214, 1131, 1614, 1138 } ) - -- - -- The above sets a collection of possible laser codes that can be assigned. **Note the { } notation!** - -- - -- ### 4.2. Auto generate laser codes - -- - -- Use the method @{#DESIGNATE.GenerateLaserCodes}() to generate all possible laser codes. Logic implemented and advised by Ciribob! - -- - -- ### 4.3. Add specific lase codes to the lase menu - -- - -- Certain plane types can only drop laser guided ordonnance when targets are lased with specific laser codes. - -- The SU-25T needs targets to be lased using laser code 1113. - -- The A-10A needs targets to be lased using laser code 1680. - -- - -- The method @{#DESIGNATE.AddMenuLaserCode}() to allow a player to lase a target using a specific laser code. - -- Remove such a lase menu option using @{#DESIGNATE.RemoveMenuLaserCode}(). - -- - -- ## 5. Autolase to automatically lase detected targets. - -- - -- DetectionItems can be auto lased once detected by Recces. As such, there is almost no action required from the Players using the Designate Menu. - -- The **auto lase** function can be activated through the Designation Menu. - -- Use the method @{#DESIGNATE.SetAutoLase}() to activate or deactivate the auto lase function programmatically. - -- Note that autolase will automatically activate lasing for ALL DetectedItems. Individual items can be switched-off if required using the Designation Menu. - -- - -- Designate:SetAutoLase( true ) - -- - -- Activate the auto lasing. - -- - -- ## 6. Target prioritization on threat level - -- - -- Targets can be detected of different types in one DetectionItem. Depending on the type of the Target, a different threat level applies in an Air to Ground combat context. - -- SAMs are of a higher threat than normal tanks. So, if the Target type was recognized, the Recces will select those targets that form the biggest threat first, - -- and will continue this until the remaining vehicles with the lowest threat have been reached. - -- - -- This threat level prioritization can be activated using the method @{#DESIGNATE.SetThreatLevelPrioritization}(). - -- If not activated, Targets will be selected in a random order, but most like those first which are the closest to the Recce marking the Target. - -- - -- Designate:SetThreatLevelPrioritization( true ) - -- - -- The example will activate the threat level prioritization for this the Designate object. Threats will be marked based on the threat level of the Target. - -- - -- ## 6. Designate Menu Location for a Mission - -- - -- You can make DESIGNATE work for a @{Mission#MISSION} object. In this way, the designate menu will not appear in the root of the radio menu, but in the menu of the Mission. - -- Use the method @{#DESIGNATE.SetMission}() to set the @{Mission} object for the designate function. - -- - -- ## 7. Status Report - -- - -- A status report is available that displays the current Targets detected, grouped per DetectionItem, and a list of which Targets are currently being marked. - -- - -- * The status report can be shown by selecting "Status" -> "Report Status" from the Designation menu . - -- * The status report can be automatically flashed by selecting "Status" -> "Flash Status On". - -- * The automatic flashing of the status report can be deactivated by selecting "Status" -> "Flash Status Off". - -- * The flashing of the status menu is disabled by default. - -- * The method @{#DESIGNATE.FlashStatusMenu}() can be used to enable or disable to flashing of the status menu. - -- - -- Designate:FlashStatusMenu( true ) - -- - -- The example will activate the flashing of the status menu for this Designate object. - -- - -- @field #DESIGNATE - DESIGNATE = { - ClassName = "DESIGNATE", - } - - --- DESIGNATE Constructor. This class is an abstract class and should not be instantiated. - -- @param #DESIGNATE self - -- @param Tasking.CommandCenter#COMMANDCENTER CC - -- @param Functional.Detection#DETECTION_BASE Detection - -- @param Core.Set#SET_GROUP AttackSet The Attack collection of GROUP objects to designate and report for. - -- @param Tasking.Mission#MISSION Mission (Optional) The Mission where the menu needs to be attached. - -- @return #DESIGNATE - function DESIGNATE:New( CC, Detection, AttackSet, Mission ) - - local self = BASE:Inherit( self, FSM:New() ) -- #DESIGNATE - self:F( { Detection } ) - - self:SetStartState( "Designating" ) - - self:AddTransition( "*", "Detect", "*" ) - --- Detect Handler OnBefore for DESIGNATE - -- @function [parent=#DESIGNATE] OnBeforeDetect - -- @param #DESIGNATE self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- Detect Handler OnAfter for DESIGNATE - -- @function [parent=#DESIGNATE] OnAfterDetect - -- @param #DESIGNATE self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- Detect Trigger for DESIGNATE - -- @function [parent=#DESIGNATE] Detect - -- @param #DESIGNATE self - - --- Detect Asynchronous Trigger for DESIGNATE - -- @function [parent=#DESIGNATE] __Detect - -- @param #DESIGNATE self - -- @param #number Delay - - self:AddTransition( "*", "LaseOn", "Lasing" ) - --- LaseOn Handler OnBefore for DESIGNATE - -- @function [parent=#DESIGNATE ] OnBeforeLaseOn - -- @param #DESIGNATE self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- LaseOn Handler OnAfter for DESIGNATE - -- @function [parent=#DESIGNATE ] OnAfterLaseOn - -- @param #DESIGNATE self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- LaseOn Trigger for DESIGNATE - -- @function [parent=#DESIGNATE ] LaseOn - -- @param #DESIGNATE self - - --- LaseOn Asynchronous Trigger for DESIGNATE - -- @function [parent=#DESIGNATE ] __LaseOn - -- @param #DESIGNATE self - -- @param #number Delay - - self:AddTransition( "Lasing", "Lasing", "Lasing" ) - - self:AddTransition( "*", "LaseOff", "Designate" ) - --- LaseOff Handler OnBefore for DESIGNATE - -- @function [parent=#DESIGNATE ] OnBeforeLaseOff - -- @param #DESIGNATE self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- LaseOff Handler OnAfter for DESIGNATE - -- @function [parent=#DESIGNATE ] OnAfterLaseOff - -- @param #DESIGNATE self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- LaseOff Trigger for DESIGNATE - -- @function [parent=#DESIGNATE ] LaseOff - -- @param #DESIGNATE self - - --- LaseOff Asynchronous Trigger for DESIGNATE - -- @function [parent=#DESIGNATE ] __LaseOff - -- @param #DESIGNATE self - -- @param #number Delay - - self:AddTransition( "*", "Smoke", "*" ) - --- Smoke Handler OnBefore for DESIGNATE - -- @function [parent=#DESIGNATE ] OnBeforeSmoke - -- @param #DESIGNATE self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- Smoke Handler OnAfter for DESIGNATE - -- @function [parent=#DESIGNATE ] OnAfterSmoke - -- @param #DESIGNATE self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- Smoke Trigger for DESIGNATE - -- @function [parent=#DESIGNATE ] Smoke - -- @param #DESIGNATE self - - --- Smoke Asynchronous Trigger for DESIGNATE - -- @function [parent=#DESIGNATE ] __Smoke - -- @param #DESIGNATE self - -- @param #number Delay - - self:AddTransition( "*", "Illuminate", "*" ) - --- Illuminate Handler OnBefore for DESIGNATE - -- @function [parent=#DESIGNATE] OnBeforeIlluminate - -- @param #DESIGNATE self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- Illuminate Handler OnAfter for DESIGNATE - -- @function [parent=#DESIGNATE] OnAfterIlluminate - -- @param #DESIGNATE self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- Illuminate Trigger for DESIGNATE - -- @function [parent=#DESIGNATE] Illuminate - -- @param #DESIGNATE self - - --- Illuminate Asynchronous Trigger for DESIGNATE - -- @function [parent=#DESIGNATE] __Illuminate - -- @param #DESIGNATE self - -- @param #number Delay - - self:AddTransition( "*", "Done", "*" ) - - self:AddTransition( "*", "Status", "*" ) - --- Status Handler OnBefore for DESIGNATE - -- @function [parent=#DESIGNATE ] OnBeforeStatus - -- @param #DESIGNATE self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- Status Handler OnAfter for DESIGNATE - -- @function [parent=#DESIGNATE ] OnAfterStatus - -- @param #DESIGNATE self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- Status Trigger for DESIGNATE - -- @function [parent=#DESIGNATE ] Status - -- @param #DESIGNATE self - - --- Status Asynchronous Trigger for DESIGNATE - -- @function [parent=#DESIGNATE ] __Status - -- @param #DESIGNATE self - -- @param #number Delay - - self.CC = CC - self.Detection = Detection - self.AttackSet = AttackSet - self.RecceSet = Detection:GetDetectionSetGroup() - self.Recces = {} - self.Designating = {} - self:SetDesignateName() - - self.LaseDuration = 60 - - self:SetFlashStatusMenu( false ) - self:SetMission( Mission ) - - self:SetLaserCodes( { 1688, 1130, 4785, 6547, 1465, 4578 } ) -- set self.LaserCodes - self:SetAutoLase( false, false ) -- set self.Autolase and don't send message. - - self:SetThreatLevelPrioritization( false ) -- self.ThreatLevelPrioritization, default is threat level priorization off - self:SetMaximumDesignations( 5 ) -- Sets the maximum designations. The default is 5 designations. - self:SetMaximumDistanceDesignations( 12000 ) -- Sets the maximum distance on which designations can be accepted. The default is 8000 meters. - self:SetMaximumMarkings( 2 ) -- Per target group, a maximum of 2 markings will be made by default. - - self:SetDesignateMenu() - - self.LaserCodesUsed = {} - - self.MenuLaserCodes = {} -- This map contains the laser codes that will be shown in the designate menu to lase with specific laser codes. - - self.Detection:__Start( 2 ) - - self:__Detect( -15 ) - - self.MarkScheduler = SCHEDULER:New( self ) - - return self - end - - --- Set the flashing of the status menu. - -- @param #DESIGNATE self - -- @param #boolean FlashMenu true: the status menu will be flashed every detection run; false: no flashing of the menu. - -- @return #DESIGNATE - function DESIGNATE:SetFlashStatusMenu( FlashMenu ) --R2.1 - - self.FlashStatusMenu = {} - - self.AttackSet:ForEachGroup( - - --- @param Wrapper.Group#GROUP GroupReport - function( AttackGroup ) - self.FlashStatusMenu[AttackGroup] = FlashMenu - end - ) - - return self - end - - - --- Set the maximum amount of designations. - -- @param #DESIGNATE self - -- @param #number MaximumDesignations - -- @return #DESIGNATE - function DESIGNATE:SetMaximumDesignations( MaximumDesignations ) - self.MaximumDesignations = MaximumDesignations - return self - end - - - --- Set the maximum ground designation distance. - -- @param #DESIGNATE self - -- @param #number MaximumDistanceGroundDesignation Maximum ground designation distance in meters. - -- @return #DESIGNATE - function DESIGNATE:SetMaximumDistanceGroundDesignation( MaximumDistanceGroundDesignation ) - self.MaximumDistanceGroundDesignation = MaximumDistanceGroundDesignation - return self - end - - - --- Set the maximum air designation distance. - -- @param #DESIGNATE self - -- @param #number MaximumDistanceAirDesignation Maximum air designation distance in meters. - -- @return #DESIGNATE - function DESIGNATE:SetMaximumDistanceAirDesignation( MaximumDistanceAirDesignation ) - self.MaximumDistanceAirDesignation = MaximumDistanceAirDesignation - return self - end - - - --- Set the overall maximum distance when designations can be accepted. - -- @param #DESIGNATE self - -- @param #number MaximumDistanceDesignations Maximum distance in meters to accept designations. - -- @return #DESIGNATE - function DESIGNATE:SetMaximumDistanceDesignations( MaximumDistanceDesignations ) - self.MaximumDistanceDesignations = MaximumDistanceDesignations - return self - end - - - --- Set the maximum amount of markings FACs will do, per designated target group. - -- @param #DESIGNATE self - -- @param #number MaximumMarkings Maximum markings FACs will do, per designated target group. - -- @return #DESIGNATE - function DESIGNATE:SetMaximumMarkings( MaximumMarkings ) - self.MaximumMarkings = MaximumMarkings - return self - end - - - --- Set an array of possible laser codes. - -- Each new lase will select a code from this table. - -- @param #DESIGNATE self - -- @param #list<#number> LaserCodes - -- @return #DESIGNATE - function DESIGNATE:SetLaserCodes( LaserCodes ) --R2.1 - - self.LaserCodes = ( type( LaserCodes ) == "table" ) and LaserCodes or { LaserCodes } - self:E( { LaserCodes = self.LaserCodes } ) - - self.LaserCodesUsed = {} - - return self - end - - - --- Add a specific lase code to the designate lase menu to lase targets with a specific laser code. - -- The MenuText will appear in the lase menu. - -- @param #DESIGNATE self - -- @param #number LaserCode The specific laser code to be added to the lase menu. - -- @param #string MenuText The text to be shown to the player. If you specify a %d in the MenuText, the %d will be replaced with the LaserCode specified. - -- @return #DESIGNATE - -- @usage - -- RecceDesignation:AddMenuLaserCode( 1113, "Lase with %d for Su-25T" ) - -- RecceDesignation:AddMenuLaserCode( 1680, "Lase with %d for A-10A" ) - -- - function DESIGNATE:AddMenuLaserCode( LaserCode, MenuText ) - - self.MenuLaserCodes[LaserCode] = MenuText - self:SetDesignateMenu() - - return self - end - - - --- Removes a specific lase code from the designate lase menu. - -- @param #DESIGNATE self - -- @param #number LaserCode The specific laser code that was set to be added to the lase menu. - -- @return #DESIGNATE - -- @usage - -- RecceDesignation:RemoveMenuLaserCode( 1113 ) - -- - function DESIGNATE:RemoveMenuLaserCode( LaserCode ) - - self.MenuLaserCodes[LaserCode] = nil - self:SetDesignateMenu() - - return self - end - - - - - --- Set the name of the designation. The name will appear in the menu. - -- This method can be used to control different designations for different plane types. - -- @param #DESIGNATE self - -- @param #string DesignateName - -- @return #DESIGNATE - function DESIGNATE:SetDesignateName( DesignateName ) - - self.DesignateName = "Designation" .. ( DesignateName and ( " for " .. DesignateName ) or "" ) - - return self - end - - - --- Generate an array of possible laser codes. - -- Each new lase will select a code from this table. - -- The entered value can range from 1111 - 1788, - -- -- but the first digit of the series must be a 1 or 2 - -- -- and the last three digits must be between 1 and 8. - -- The range used to be bugged so its not 1 - 8 but 0 - 7. - -- function below will use the range 1-7 just in case - -- @param #DESIGNATE self - -- @return #DESIGNATE - function DESIGNATE:GenerateLaserCodes() --R2.1 - - self.LaserCodes = {} - - local function containsDigit(_number, _numberToFind) - - local _thisNumber = _number - local _thisDigit = 0 - - while _thisNumber ~= 0 do - _thisDigit = _thisNumber % 10 - _thisNumber = math.floor(_thisNumber / 10) - if _thisDigit == _numberToFind then - return true - end - end - - return false - end - - -- generate list of laser codes - local _code = 1111 - local _count = 1 - while _code < 1777 and _count < 30 do - while true do - _code = _code + 1 - if not containsDigit(_code, 8) - and not containsDigit(_code, 9) - and not containsDigit(_code, 0) then - self:T(_code) - table.insert( self.LaserCodes, _code ) - break - end - end - _count = _count + 1 - end - - self.LaserCodesUsed = {} - - return self - end - - - - --- Set auto lase. - -- Auto lase will start lasing targets immediately when these are in range. - -- @param #DESIGNATE self - -- @param #boolean AutoLase (optional) true sets autolase on, false off. Default is off. - -- @param #boolean Message (optional) true is send message, false or nil won't send a message. Default is no message sent. - -- @return #DESIGNATE - function DESIGNATE:SetAutoLase( AutoLase, Message ) - - self.AutoLase = AutoLase or false - - if Message then - local AutoLaseOnOff = ( self.AutoLase == true ) and "On" or "Off" - local CC = self.CC:GetPositionable() - if CC then - CC:MessageToSetGroup( self.DesignateName .. ": Auto Lase " .. AutoLaseOnOff .. ".", 15, self.AttackSet ) - end - end - - self:CoordinateLase() - self:SetDesignateMenu() - - return self - end - - --- Set priorization of Targets based on the **Threat Level of the Target** in an Air to Ground context. - -- @param #DESIGNATE self - -- @param #boolean Prioritize - -- @return #DESIGNATE - function DESIGNATE:SetThreatLevelPrioritization( Prioritize ) --R2.1 - - self.ThreatLevelPrioritization = Prioritize - - return self - end - - --- Set the MISSION object for which designate will function. - -- When a MISSION object is assigned, the menu for the designation will be located at the Mission Menu. - -- @param #DESIGNATE self - -- @param Tasking.Mission#MISSION Mission The MISSION object. - -- @return #DESIGNATE - function DESIGNATE:SetMission( Mission ) --R2.2 - - self.Mission = Mission - - return self - end - - - --- - -- @param #DESIGNATE self - -- @return #DESIGNATE - function DESIGNATE:onafterDetect() - - self:__Detect( -math.random( 60 ) ) - - self:DesignationScope() - self:CoordinateLase() - self:SendStatus() - self:SetDesignateMenu() - - return self - end - - - --- Adapt the designation scope according the detected items. - -- @param #DESIGNATE self - -- @return #DESIGNATE - function DESIGNATE:DesignationScope() - - local DetectedItems = self.Detection:GetDetectedItems() - - local DetectedItemCount = 0 - - for DesignateIndex, Designating in pairs( self.Designating ) do - local DetectedItem = DetectedItems[DesignateIndex] - if DetectedItem then - -- Check LOS... - local IsDetected = self.Detection:IsDetectedItemDetected( DetectedItem ) - self:F({IsDetected = IsDetected, DetectedItem }) - if IsDetected == false then - self:F("Removing") - -- This Detection is obsolete, remove from the designate scope - self.Designating[DesignateIndex] = nil - self.AttackSet:ForEachGroup( - function( AttackGroup ) - local DetectionText = self.Detection:DetectedItemReportSummary( DesignateIndex, AttackGroup ):Text( ", " ) - self.CC:GetPositionable():MessageToGroup( "Targets out of LOS\n" .. DetectionText, 10, AttackGroup, self.DesignateName ) - end - ) - else - DetectedItemCount = DetectedItemCount + 1 - end - else - -- This Detection is obsolete, remove from the designate scope - self.Designating[DesignateIndex] = nil - end - end - - if DetectedItemCount < 5 then - for DesignateIndex, DetectedItem in pairs( DetectedItems ) do - local IsDetected = self.Detection:IsDetectedItemDetected( DetectedItem ) - if IsDetected == true then - self:F( { DistanceRecce = DetectedItem.DistanceRecce } ) - if DetectedItem.DistanceRecce <= self.MaximumDistanceDesignations then - if self.Designating[DesignateIndex] == nil then - -- ok, we added one item to the designate scope. - self.AttackSet:ForEachGroup( - function( AttackGroup ) - local DetectionText = self.Detection:DetectedItemReportSummary( DesignateIndex, AttackGroup ):Text( ", " ) - self.CC:GetPositionable():MessageToGroup( "Targets detected at \n" .. DetectionText, 10, AttackGroup, self.DesignateName ) - end - ) - self.Designating[DesignateIndex] = "" - break - end - end - end - end - end - - return self - end - - --- Coordinates the Auto Lase. - -- @param #DESIGNATE self - -- @return #DESIGNATE - function DESIGNATE:CoordinateLase() - - local DetectedItems = self.Detection:GetDetectedItems() - - for DesignateIndex, Designating in pairs( self.Designating ) do - local DetectedItem = DetectedItems[DesignateIndex] - if DetectedItem then - if self.AutoLase then - self:LaseOn( DesignateIndex, self.LaseDuration ) - end - end - end - - return self - end - - - --- Sends the status to the Attack Groups. - -- @param #DESIGNATE self - -- @param Wrapper.Group#GROUP AttackGroup - -- @param #number Duration The time in seconds the report should be visible. - -- @return #DESIGNATE - function DESIGNATE:SendStatus( MenuAttackGroup, Duration ) - - Duration = Duration or 10 - - self.AttackSet:ForEachGroup( - - --- @param Wrapper.Group#GROUP GroupReport - function( AttackGroup ) - - if self.FlashStatusMenu[AttackGroup] or ( MenuAttackGroup and ( AttackGroup:GetName() == MenuAttackGroup:GetName() ) ) then - - local DetectedReport = REPORT:New( "Targets ready for Designation:" ) - local DetectedItems = self.Detection:GetDetectedItems() - - for DesignateIndex, Designating in pairs( self.Designating ) do - local DetectedItem = DetectedItems[DesignateIndex] - if DetectedItem then - local Report = self.Detection:DetectedItemReportSummary( DesignateIndex, AttackGroup ):Text( ", " ) - DetectedReport:Add( string.rep( "-", 140 ) ) - DetectedReport:Add( " - " .. Report ) - end - end - - local CC = self.CC:GetPositionable() - - CC:MessageToGroup( DetectedReport:Text( "\n" ), Duration, AttackGroup, self.DesignateName ) - - local DesignationReport = REPORT:New( "Marking Targets:\n" ) - - self.RecceSet:ForEachGroup( - function( RecceGroup ) - local RecceUnits = RecceGroup:GetUnits() - for UnitID, RecceData in pairs( RecceUnits ) do - local Recce = RecceData -- Wrapper.Unit#UNIT - if Recce:IsLasing() then - DesignationReport:Add( " - " .. Recce:GetMessageText( "Marking " .. Recce:GetSpot().Target:GetTypeName() .. " with laser " .. Recce:GetSpot().LaserCode .. "." ) ) - end - end - end - ) - - CC:MessageToGroup( DesignationReport:Text(), Duration, AttackGroup, self.DesignateName ) - end - end - ) - - return self - end - - --- Sets the Designate Menu. - -- @param #DESIGNATE self - -- @return #DESIGNATE - function DESIGNATE:SetDesignateMenu() - - self.AttackSet:Flush() - - self.AttackSet:ForEachGroup( - - --- @param Wrapper.Group#GROUP GroupReport - function( AttackGroup ) - self.MenuDesignate = self.MenuDesignate or {} - - local MissionMenu = nil - - if self.Mission then - MissionMenu = self.Mission:GetRootMenu( AttackGroup ) - end - - local MenuTime = timer.getTime() - - self.MenuDesignate[AttackGroup] = MENU_GROUP:New( AttackGroup, self.DesignateName, MissionMenu ):SetTime( MenuTime ):SetTag( self.DesignateName ) - local MenuDesignate = self.MenuDesignate[AttackGroup] -- Core.Menu#MENU_GROUP - - -- Set Menu option for auto lase - - if self.AutoLase then - MENU_GROUP_COMMAND:New( AttackGroup, "Auto Lase Off", MenuDesignate, self.MenuAutoLase, self, false ):SetTime( MenuTime ):SetTag( self.DesignateName ) - else - MENU_GROUP_COMMAND:New( AttackGroup, "Auto Lase On", MenuDesignate, self.MenuAutoLase, self, true ):SetTime( MenuTime ):SetTag( self.DesignateName ) - end - - local StatusMenu = MENU_GROUP:New( AttackGroup, "Status", MenuDesignate ):SetTime( MenuTime ):SetTag( self.DesignateName ) - MENU_GROUP_COMMAND:New( AttackGroup, "Report Status 15s", StatusMenu, self.MenuStatus, self, AttackGroup, 15 ):SetTime( MenuTime ):SetTag( self.DesignateName ) - MENU_GROUP_COMMAND:New( AttackGroup, "Report Status 30s", StatusMenu, self.MenuStatus, self, AttackGroup, 30 ):SetTime( MenuTime ):SetTag( self.DesignateName ) - MENU_GROUP_COMMAND:New( AttackGroup, "Report Status 60s", StatusMenu, self.MenuStatus, self, AttackGroup, 60 ):SetTime( MenuTime ):SetTag( self.DesignateName ) - - if self.FlashStatusMenu[AttackGroup] then - MENU_GROUP_COMMAND:New( AttackGroup, "Flash Status Report Off", StatusMenu, self.MenuFlashStatus, self, AttackGroup, false ):SetTime( MenuTime ):SetTag( self.DesignateName ) - else - MENU_GROUP_COMMAND:New( AttackGroup, "Flash Status Report On", StatusMenu, self.MenuFlashStatus, self, AttackGroup, true ):SetTime( MenuTime ):SetTag( self.DesignateName ) - end - - for DesignateIndex, Designating in pairs( self.Designating ) do - - local DetectedItem = self.Detection:GetDetectedItem( DesignateIndex ) - - if DetectedItem then - - local Coord = self.Detection:GetDetectedItemCoordinate( DesignateIndex ) - local ID = self.Detection:GetDetectedItemID( DesignateIndex ) - local MenuText = ID .. ", " .. Coord:ToStringA2G( AttackGroup ) - - if Designating == "" then - MenuText = "(-) " .. MenuText - local DetectedMenu = MENU_GROUP:New( AttackGroup, MenuText, MenuDesignate ):SetTime( MenuTime ):SetTag( self.DesignateName ) - MENU_GROUP_COMMAND:New( AttackGroup, "Search other target", DetectedMenu, self.MenuForget, self, DesignateIndex ):SetTime( MenuTime ):SetTag( self.DesignateName ) - for LaserCode, MenuText in pairs( self.MenuLaserCodes ) do - MENU_GROUP_COMMAND:New( AttackGroup, string.format( MenuText, LaserCode ), DetectedMenu, self.MenuLaseCode, self, DesignateIndex, 60, LaserCode ):SetTime( MenuTime ):SetTag( self.DesignateName ) - end - MENU_GROUP_COMMAND:New( AttackGroup, "Lase with random laser code(s)", DetectedMenu, self.MenuLaseOn, self, DesignateIndex, 60 ):SetTime( MenuTime ):SetTag( self.DesignateName ) - MENU_GROUP_COMMAND:New( AttackGroup, "Smoke red", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.Red ):SetTime( MenuTime ):SetTag( self.DesignateName ) - MENU_GROUP_COMMAND:New( AttackGroup, "Smoke blue", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.Blue ):SetTime( MenuTime ):SetTag( self.DesignateName ) - MENU_GROUP_COMMAND:New( AttackGroup, "Smoke green", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.Green ):SetTime( MenuTime ):SetTag( self.DesignateName ) - MENU_GROUP_COMMAND:New( AttackGroup, "Smoke white", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.White ):SetTime( MenuTime ):SetTag( self.DesignateName ) - MENU_GROUP_COMMAND:New( AttackGroup, "Smoke orange", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.Orange ):SetTime( MenuTime ):SetTag( self.DesignateName ) - MENU_GROUP_COMMAND:New( AttackGroup, "Illuminate", DetectedMenu, self.MenuIlluminate, self, DesignateIndex ):SetTime( MenuTime ):SetTag( self.DesignateName ) - else - if Designating == "Laser" then - MenuText = "(L) " .. MenuText - elseif Designating == "Smoke" then - MenuText = "(S) " .. MenuText - elseif Designating == "Illuminate" then - MenuText = "(I) " .. MenuText - end - local DetectedMenu = MENU_GROUP:New( AttackGroup, MenuText, MenuDesignate ):SetTime( MenuTime ):SetTag( self.DesignateName ) - if Designating == "Laser" then - MENU_GROUP_COMMAND:New( AttackGroup, "Stop lasing", DetectedMenu, self.MenuLaseOff, self, DesignateIndex ):SetTime( MenuTime ):SetTag( self.DesignateName ) - else - end - end - end - end - MenuDesignate:Remove( MenuTime, self.DesignateName ) - end - ) - - return self - end - - --- - -- @param #DESIGNATE self - function DESIGNATE:MenuStatus( AttackGroup, Duration ) - - self:E("Status") - - self:SendStatus( AttackGroup, Duration ) - end - - --- - -- @param #DESIGNATE self - function DESIGNATE:MenuFlashStatus( AttackGroup, Flash ) - - self:E("Flash Status") - - self.FlashStatusMenu[AttackGroup] = Flash - self:SetDesignateMenu() - end - - - --- - -- @param #DESIGNATE self - function DESIGNATE:MenuForget( Index ) - - self:E("Forget") - - self.Designating[Index] = nil - self:SetDesignateMenu() - end - - --- - -- @param #DESIGNATE self - function DESIGNATE:MenuAutoLase( AutoLase ) - - self:E("AutoLase") - - self:SetAutoLase( AutoLase, true ) - end - - --- - -- @param #DESIGNATE self - function DESIGNATE:MenuSmoke( Index, Color ) - - self:E("Designate through Smoke") - - self.Designating[Index] = "Smoke" - self:Smoke( Index, Color ) - end - - --- - -- @param #DESIGNATE self - function DESIGNATE:MenuIlluminate( Index ) - - self:E("Designate through Illumination") - - self.Designating[Index] = "Illuminate" - - self:__Illuminate( 1, Index ) - end - - --- - -- @param #DESIGNATE self - function DESIGNATE:MenuLaseOn( Index, Duration ) - - self:E("Designate through Lase") - - self:__LaseOn( 1, Index, Duration ) - self:SetDesignateMenu() - end - - - --- - -- @param #DESIGNATE self - function DESIGNATE:MenuLaseCode( Index, Duration, LaserCode ) - - self:E( "Designate through Lase using " .. LaserCode ) - - self:__LaseOn( 1, Index, Duration, LaserCode ) - self:SetDesignateMenu() - end - - - --- - -- @param #DESIGNATE self - function DESIGNATE:MenuLaseOff( Index, Duration ) - - self:E("Lasing off") - - self.Designating[Index] = "" - self:__LaseOff( 1, Index ) - self:SetDesignateMenu() - end - - --- - -- @param #DESIGNATE self - function DESIGNATE:onafterLaseOn( From, Event, To, Index, Duration, LaserCode ) - - self.Designating[Index] = "Laser" - self.LaseStart = timer.getTime() - self.LaseDuration = Duration - self:__Lasing( -1, Index, Duration, LaserCode ) - end - - - --- - -- @param #DESIGNATE self - -- @return #DESIGNATE - function DESIGNATE:onafterLasing( From, Event, To, Index, Duration, LaserCodeRequested ) - - - local TargetSetUnit = self.Detection:GetDetectedSet( Index ) - - local MarkingCount = 0 - local MarkedTypes = {} - local ReportTypes = REPORT:New() - local ReportLaserCodes = REPORT:New() - - TargetSetUnit:Flush() - - --self:F( { Recces = self.Recces } ) - for TargetUnit, RecceData in pairs( self.Recces ) do - local Recce = RecceData -- Wrapper.Unit#UNIT - self:F( { TargetUnit = TargetUnit, Recce = Recce:GetName() } ) - if not Recce:IsLasing() then - local LaserCode = Recce:GetLaserCode() -- (Not deleted when stopping with lasing). - self:F( { ClearingLaserCode = LaserCode } ) - self.LaserCodesUsed[LaserCode] = nil - self.Recces[TargetUnit] = nil - end - end - - -- If a specific lasercode is requested, we disable one active lase! - if LaserCodeRequested then - for TargetUnit, RecceData in pairs( self.Recces ) do -- We break after the first has been processed. - local Recce = RecceData -- Wrapper.Unit#UNIT - self:F( { TargetUnit = TargetUnit, Recce = Recce:GetName() } ) - if Recce:IsLasing() then - -- When a Recce is lasing, we switch the lasing off, and clear the references to the lasing in the DESIGNATE class. - Recce:LaseOff() -- Switch off the lasing. - local LaserCode = Recce:GetLaserCode() -- (Not deleted when stopping with lasing). - self:F( { ClearingLaserCode = LaserCode } ) - self.LaserCodesUsed[LaserCode] = nil - self.Recces[TargetUnit] = nil - break - end - end - end - - if self.AutoLase or ( not self.AutoLase and ( self.LaseStart + Duration >= timer.getTime() ) ) then - - TargetSetUnit:ForEachUnitPerThreatLevel( 10, 0, - --- @param Wrapper.Unit#UNIT SmokeUnit - function( TargetUnit ) - - self:F( { TargetUnit = TargetUnit:GetName() } ) - - if MarkingCount < self.MaximumMarkings then - - if TargetUnit:IsAlive() then - - local Recce = self.Recces[TargetUnit] - - if not Recce then - - self:E( "Lasing..." ) - self.RecceSet:Flush() - - for RecceGroupID, RecceGroup in pairs( self.RecceSet:GetSet() ) do - for UnitID, UnitData in pairs( RecceGroup:GetUnits() or {} ) do - - local RecceUnit = UnitData -- Wrapper.Unit#UNIT - local RecceUnitDesc = RecceUnit:GetDesc() - --self:F( { RecceUnit = RecceUnit:GetName(), RecceDescription = RecceUnitDesc } ) - - if RecceUnit:IsLasing() == false then - --self:F( { IsDetected = RecceUnit:IsDetected( TargetUnit ), IsLOS = RecceUnit:IsLOS( TargetUnit ) } ) - - if RecceUnit:IsDetected( TargetUnit ) and RecceUnit:IsLOS( TargetUnit ) then - - local LaserCodeIndex = math.random( 1, #self.LaserCodes ) - local LaserCode = self.LaserCodes[LaserCodeIndex] - --self:F( { LaserCode = LaserCode, LaserCodeUsed = self.LaserCodesUsed[LaserCode] } ) - - if LaserCodeRequested and LaserCodeRequested ~= LaserCode then - LaserCode = LaserCodeRequested - LaserCodeRequested = nil - end - - if not self.LaserCodesUsed[LaserCode] then - - self.LaserCodesUsed[LaserCode] = LaserCodeIndex - local Spot = RecceUnit:LaseUnit( TargetUnit, LaserCode, Duration ) - local AttackSet = self.AttackSet - - function Spot:OnAfterDestroyed( From, Event, To ) - self:E( "Destroyed Message" ) - self.Recce:ToSetGroup( "Target " .. TargetUnit:GetTypeName() .. " destroyed. " .. TargetSetUnit:Count() .. " targets left.", 5, AttackSet, self.DesignateName ) - end - - self.Recces[TargetUnit] = RecceUnit - RecceUnit:MessageToSetGroup( "Marking " .. TargetUnit:GetTypeName() .. " with laser " .. RecceUnit:GetSpot().LaserCode .. " for " .. Duration .. "s.", 5, self.AttackSet, self.DesignateName ) - -- OK. We have assigned for the Recce a TargetUnit. We can exit the function. - MarkingCount = MarkingCount + 1 - local TargetUnitType = TargetUnit:GetTypeName() - if not MarkedTypes[TargetUnitType] then - MarkedTypes[TargetUnitType] = true - ReportTypes:Add(TargetUnitType) - end - ReportLaserCodes:Add(RecceUnit.LaserCode) - return - end - else - --RecceUnit:MessageToSetGroup( "Can't mark " .. TargetUnit:GetTypeName(), 5, self.AttackSet ) - end - else - -- The Recce is lasing, but the Target is not detected or within LOS. So stop lasing and send a report. - - if not RecceUnit:IsDetected( TargetUnit ) or not RecceUnit:IsLOS( TargetUnit ) then - - local Recce = self.Recces[TargetUnit] -- Wrapper.Unit#UNIT - - if Recce then - Recce:LaseOff() - Recce:MessageToSetGroup( "Target " .. TargetUnit:GetTypeName() "out of LOS. Cancelling lase!", 5, self.AttackSet, self.DesignateName ) - end - else - MarkingCount = MarkingCount + 1 - local TargetUnitType = TargetUnit:GetTypeName() - if not MarkedTypes[TargetUnitType] then - MarkedTypes[TargetUnitType] = true - ReportTypes:Add(TargetUnitType) - end - ReportLaserCodes:Add(RecceUnit.LaserCode) - end - end - end - end - else - MarkingCount = MarkingCount + 1 - local TargetUnitType = TargetUnit:GetTypeName() - if not MarkedTypes[TargetUnitType] then - MarkedTypes[TargetUnitType] = true - ReportTypes:Add(TargetUnitType) - end - ReportLaserCodes:Add(Recce.LaserCode) - --Recce:MessageToSetGroup( self.DesignateName .. ": Marking " .. TargetUnit:GetTypeName() .. " with laser " .. Recce.LaserCode .. ".", 5, self.AttackSet ) - end - end - end - end - ) - - local MarkedTypesText = ReportTypes:Text(', ') - local MarkedLaserCodesText = ReportLaserCodes:Text(', ') - for MarkedType, MarketCount in pairs( MarkedTypes ) do - self.CC:GetPositionable():MessageToSetGroup( "Marking " .. MarkingCount .. " x " .. MarkedTypesText .. " with lasers " .. MarkedLaserCodesText .. ".", 5, self.AttackSet, self.DesignateName ) - end - - self:__Lasing( -30, Index, Duration, LaserCodeRequested ) - - self:SetDesignateMenu() - - else - self:__LaseOff( 1 ) - end - - end - - --- - -- @param #DESIGNATE self - -- @return #DESIGNATE - function DESIGNATE:onafterLaseOff( From, Event, To, Index ) - - local CC = self.CC:GetPositionable() - - if CC then - CC:MessageToSetGroup( "Stopped lasing.", 5, self.AttackSet, self.DesignateName ) - end - - local TargetSetUnit = self.Detection:GetDetectedSet( Index ) - - local Recces = self.Recces - - for TargetID, RecceData in pairs( Recces ) do - local Recce = RecceData -- Wrapper.Unit#UNIT - Recce:MessageToSetGroup( "Stopped lasing " .. Recce:GetSpot().Target:GetTypeName() .. ".", 5, self.AttackSet, self.DesignateName ) - Recce:LaseOff() - end - - Recces = nil - self.Recces = {} - self.LaserCodesUsed = {} - - self:SetDesignateMenu() - end - - - --- - -- @param #DESIGNATE self - -- @return #DESIGNATE - function DESIGNATE:onafterSmoke( From, Event, To, Index, Color ) - - local TargetSetUnit = self.Detection:GetDetectedSet( Index ) - local TargetSetUnitCount = TargetSetUnit:Count() - - local MarkedCount = 0 - - TargetSetUnit:ForEachUnitPerThreatLevel( 10, 0, - --- @param Wrapper.Unit#UNIT SmokeUnit - function( SmokeUnit ) - - if MarkedCount < self.MaximumMarkings then - - MarkedCount = MarkedCount + 1 - - self:E( "Smoking ..." ) - - local RecceGroup = self.RecceSet:FindNearestGroupFromPointVec2(SmokeUnit:GetPointVec2()) - local RecceUnit = RecceGroup:GetUnit( 1 ) - - if RecceUnit then - - RecceUnit:MessageToSetGroup( "Smoking " .. SmokeUnit:GetTypeName() .. ".", 5, self.AttackSet, self.DesignateName ) - - self.MarkScheduler:Schedule( self, - function() - if SmokeUnit:IsAlive() then - SmokeUnit:Smoke( Color, 50, 2 ) - end - self:Done( Index ) - end, {}, math.random( 5, 20 ) - ) - end - end - end - ) - - - end - - --- Illuminating - -- @param #DESIGNATE self - -- @return #DESIGNATE - function DESIGNATE:onafterIlluminate( From, Event, To, Index ) - - local TargetSetUnit = self.Detection:GetDetectedSet( Index ) - local TargetUnit = TargetSetUnit:GetFirst() - - if TargetUnit then - local RecceGroup = self.RecceSet:FindNearestGroupFromPointVec2(TargetUnit:GetPointVec2()) - local RecceUnit = RecceGroup:GetUnit( 1 ) - if RecceUnit then - RecceUnit:MessageToSetGroup( "Illuminating " .. TargetUnit:GetTypeName() .. ".", 5, self.AttackSet, self.DesignateName ) - self.MarkScheduler:Schedule( self, - function() - if TargetUnit:IsAlive() then - TargetUnit:GetPointVec3():AddY(300):IlluminationBomb() - end - self:Done( Index ) - end, {}, math.random( 5, 20 ) - ) - end - end - end - - --- Done - -- @param #DESIGNATE self - -- @return #DESIGNATE - function DESIGNATE:onafterDone( From, Event, To, Index ) - - self.Designating[Index] = nil - self:SetDesignateMenu() - end - -end - --- Help from Ciribob - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- **Functional** - Create random airtraffic in your missions. --- --- ![Banner Image](..\Presentations\RAT\RAT.png) --- --- ==== --- --- The aim of the RAT class is to fill the empty DCS world with randomized air traffic and bring more life to your airports. --- --- In particular, it is designed to spawn AI air units at random airports. These units will be assigned a random flight path to another random airport on the map. --- --- Even the mission designer will not know where aircraft will be spawned and which route they follow. --- --- ## Features --- --- * Very simple interface. Just one unit and two lines of Lua code needed to fill your map. --- * High degree of randomization. Aircraft will spawn at random airports, have random routes and random destinations. --- * Specific departure and/or destination airports can be chosen. --- * Departure and destination airports can be restricted by coalition. --- * Planes and helicopters supported. Helicopters can also be send to FARPs and ships. --- * Units can also be spawned in air within pre-defined zones of the map. --- * Aircraft will be removed when they arrive at their destination (or get stuck on the ground). --- * When a unit is removed a new unit with a different flight plan is respawned. --- * Aircraft can report their status during the route. --- * All of the above can be customized by the user if necessary. --- * All current (Caucasus, Nevada, Normandy) and future maps are supported. --- --- The RAT class creates an entry in the F10 menu which allows to --- --- * Create new groups on-the-fly, i.e. at run time within the mission, --- * Destroy specific groups (e.g. if they get stuck or damaged and block a runway), --- * Request the status of all RAT aircraft or individual groups, --- * Place markers at waypoints on the F10 map for each group. --- --- Note that by its very nature, this class is suited best for civil or transport aircraft. However, it also works perfectly fine for military aircraft of any kind. --- --- More of the documentation include some simple examples can be found further down this page. --- --- ==== --- --- # Demo Missions --- --- ### [RAT Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/Release/RAT%20-%20Random%20Air%20Traffic) --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) --- --- ==== --- --- # YouTube Channel --- --- ### RAT videos are work in progress. --- ### [MOOSE YouTube Channel](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl1jirWIo4t4YxqN-HxjqRkL) --- --- === --- --- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** --- --- ### Contributions: **Sven van de Velde ([FlightControl](https://forums.eagle.ru/member.php?u=89536))** --- --- ==== --- @module Rat - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- RAT class --- @type RAT --- @field #string ClassName Name of the Class. --- @field #boolean debug Turn debug messages on or off. --- @field #string alias Alias for spawned group. --- @field #number spawndelay Delay time in seconds before first spawning happens. --- @field #number spawninterval Interval between spawning units/groups. Note that we add a randomization of 50%. --- @field #number coalition Coalition of spawn group template. --- @field #number country Country of spawn group template. --- @field #string category Category of aircarft: "plane" or "heli". --- @field #string friendly Possible departure/destination airport: all=blue+red+neutral, same=spawn+neutral, spawnonly=spawn, blue=blue+neutral, blueonly=blue, red=red+neutral, redonly=red. --- @field #table ctable Table with the valid coalitons from choice self.friendly. --- @field #table aircraft Table which holds the basic aircraft properties (speed, range, ...). --- @field #number Vcruisemax Max cruise speed in m/s (250 m/s = 900 km/h = 486 kt) set by user. --- @field #number Vclimb Default climb rate in ft/min. --- @field #number AlphaDescent Default angle of descenti in degrees. A value of 3.6 follows the 3:1 rule of 3 miles of travel and 1000 ft descent. --- @field #string roe ROE of spawned groups, default is weapon hold (this is a peaceful class for civil aircraft or ferry missions). Possible: "hold", "return", "free". --- @field #string rot ROT of spawned groups, default is no reaction. Possible: "noreaction", "passive", "evade". --- @field #string takeoff Takeoff type. 0=coldorhot. --- @field #number mindist Min distance from departure to destination in meters. Default 5 km. --- @field #number maxdist Max distance from departure to destination in meters. Default 5000 km. --- @field #table airports_map All airports available on current map (Caucasus, Nevada, Normandy, ...). --- @field #table airports All airports of friedly coalitions. --- @field #boolean random_departure By default a random friendly airport is chosen as departure. --- @field #boolean random_destination By default a random friendly airport is chosen as destination. --- @field #table departure_zones Array containing the names of the departure zones. --- @field #table departure_ports Array containing the names of the destination airports. --- @field #table destination_ports Array containing the names of the destination airports. --- @field #table ratcraft Array with the spawned RAT aircraft. --- @field #number Tinactive Time in seconds after which inactive units will be destroyed. Default is 300 seconds. --- @field #boolean reportstatus Aircraft report status. --- @field #number statusinterval Intervall between status checks (and reports if enabled). --- @field #boolean placemarkers Place markers of waypoints on F10 map. --- @field #number FLuser Flight level set by users explicitly. --- @field #number FLminuser Minimum flight level set by user. --- @field #number FLmaxuser Maximum flight level set by user. --- @field #boolean commute Aircraft commute between departure and destination, i.e. when respawned the departure airport becomes the new destiation. --- @field #boolean continuejourney Aircraft will continue their journey, i.e. get respawned at their destination with a new random destination. --- @field #number ngroups Number of groups to be spawned in total. --- @field #number alive Number of groups which are alive. --- @field #boolean f10menu Add an F10 menu for RAT. --- @field #table Menu F10 menu items for this RAT object. --- @field #string SubMenuName Submenu name for RAT object. --- @field #boolean respawn_at_landing Respawn aircraft the moment they land rather than at engine shutdown. --- @field #number respawn_delay Delay in seconds until repawn happens after landing. --- @field #table markerids Array with marker IDs. --- @field #string livery Livery of the aircraft set by user. --- @field #string skill Skill of AI. --- @field #boolean ATCswitch Enable/disable ATC if set to true/false. --- @extends Functional.Spawn#SPAWN - ----# RAT class, extends @{Spawn#SPAWN} --- The RAT class implements an easy to use way to randomly fill your map with AI aircraft. --- --- --- ## Airport Selection --- --- ![Process](..\Presentations\RAT\RAT_Airport_Selection.png) --- --- ### Default settings: --- --- * By default, aircraft are spawned at airports of their own coalition (blue or red) or neutral airports. --- * Destination airports are by default also of neutral or of the same coalition as the template group of the spawned aircraft. --- * Possible destinations are restricted by their distance to the departure airport. The maximal distance depends on the max range of spawned aircraft type and its initial fuel amount. --- --- ### The default behavior can be changed: --- --- * A specific departure and/or destination airport can be chosen. --- * Valid coalitions can be set, e.g. only red, blue or neutral, all three "colours". --- * It is possible to start in air within a zone defined in the mission editor or within a zone above an airport of the map. --- --- ## Flight Plan --- --- ![Process](..\Presentations\RAT\RAT_Flight_Plan.png) --- --- * A general flight plan has five main airborne segments: Climb, cruise, descent, holding and final approach. --- * Events monitored during the flight are: birth, engine-start, take-off, landing and engine-shutdown. --- * The default flight level (FL) is set to ~FL200, i.e. 20000 feet ASL but randomized for each aircraft. --- Service ceiling of aircraft type is into account for max FL as well as the distance between departure and destination. --- * Maximal distance between destination and departure airports depends on range and initial fuel of aircraft. --- * Climb rate is set to a moderate value of ~1500 ft/min. --- * The standard descent rate follows the 3:1 rule, i.e. 1000 ft decent per 3 miles of travel. Hence, angle of descent is ~3.6 degrees. --- * A holding point is randomly selected at a distance between 5 and 10 km away from destination airport. --- * The altitude of theholding point is ~1200 m AGL. Holding patterns might or might not happen with variable duration. --- * If an aircraft is spawned in air, the procedure omitts taxi and take-off and starts with the climb/cruising part. --- * All values are randomized for each spawned aircraft. --- --- ## Mission Editor Setup --- --- ![Process](..\Presentations\RAT\RAT_Mission_Setup.png) --- --- Basic mission setup is very simple and essentially a three step process: --- --- * Place your aircraft **anywhere** on the map. It really does not matter where you put it. --- * Give the group a good name. In the example above the group is named "RAT_YAK". --- * Activate the "LATE ACTIVATION" tick box. Note that this aircraft will not be spawned itself but serves a template for each RAT aircraft spawned when the mission starts. --- --- Voilà, your already done! --- --- Optionally, you can set a specific livery for the aircraft or give it some weapons. --- However, the aircraft will by default not engage any enemies. Think of them as beeing on a peaceful or ferry mission. --- --- ## Basic Lua Script --- --- ![Process](..\Presentations\RAT\RAT_Basic_Lua_Script.png) --- --- The basic Lua script for one template group consits of two simple lines as shown in the picture above. --- --- * **Line 2** creates a new RAT object "yak". The only required parameter for the constructor @{#RAT.New}() is the name of the group as defined in the mission editor. In this example it is "RAT_YAK". --- * **Line 5** trigger the command to spawn the aircraft. The (optional) parameter for the @{#RAT.Spawn}() function is the number of aircraft to be spawned of this object. --- By default each of these aircraft gets a random departure airport anywhere on the map and a random destination airport, which lies within range of the of the selected aircraft type. --- --- In this simple example aircraft are respawned with a completely new flightplan when they have reached their destination airport. --- The "old" aircraft is despawned (destroyed) after it has shut-down its engines and a new aircraft of the same type is spawned at a random departure airport anywhere on the map. --- Hence, the default flight plan for a RAT aircraft will be: Fly from airport A to B, get respawned at C and fly to D, get respawned at E and fly to F, ... --- This ensures that you always have a constant number of AI aircraft on your map. --- --- ## Examples --- --- Here are a few examples, how you can modify the default settings of RAT class objects. --- --- ### Specify Departure and Destinations --- --- ![Process](..\Presentations\RAT\RAT_Examples_Specify_Departure_and_Destination.png) --- --- In the picture above you find a few possibilities how to modify the default behaviour to spawn at random airports and fly to random destinations. --- --- In particular, you can specify fixed departure and/or destination airports. This is done via the @{#RAT.SetDeparture}() or @{#RAT.SetDestination}() functions, respectively. --- --- * If you only fix a specific departure airport via @{#RAT.SetDeparture}() all aircraft will be spawned at that airport and get random destination airports. --- * If you only fix the destination airport via @{#RAT.SetDestination}(), aircraft a spawned at random departure airports but will all fly to the destination airport. --- * If you fix departure and destination airports, aircraft will only travel from between those airports. --- When the aircraft reaches its destination, it will be respawned at its departure and fly again to its destination. --- --- There is also an option that allows aircraft to "continue their journey" from their destination. This is achieved by the @{#RAT.ContinueJourney}() function. --- In that case, when the aircraft arrives at its first destination it will be respawned at that very airport and get a new random destination. --- So the flight plan in this case would be: Fly from airport A to B, then from B to C, then from C to D, ... --- --- It is also possible to make aircraft "commute" between two airports, i.e. flying from airport A to B and then back from B to A, etc. --- This can be done by the @{#RAT.Commute}() function. Note that if no departure or destination airports are specified, the first departure and destination are chosen randomly. --- Then the aircraft will fly back and forth between those two airports indefinetly. --- --- --- ### Spawn in Air --- --- ![Process](..\Presentations\RAT\RAT_Examples_Spawn_in_Air.png) --- --- Aircraft can also be spawned in air rather than at airports on the ground. This is done by setting @{#RAT.SetTakeoff}() to "air". --- --- By default, aircraft are spawned randomly above airports of the map. --- --- The @{#RAT.SetDeparture}() option can be used to specify zones, which have been defined in the mission editor as departure zones. --- Aircraft will then be spawned at a random point within the zone or zones. --- --- Note that @{#RAT.SetDeparture}() also accepts airport names. For an air takeoff these are treated like zones with a radius of XX kilometers. --- Again, aircraft are spawned at random points within these zones around the airport. --- --- ### Misc Options --- --- ![Process](..\Presentations\RAT\RAT_Examples_Misc.png) --- --- The default "takeoff" type of RAT aircraft is that they are spawned with hot or cold engines. --- The choice is random, so 50% of aircraft will be spawned with hot engines while the other 50% will be spawned with cold engines. --- This setting can be changed using the @{#RAT.SetTakeoff}() function. The possible parameters for starting on ground are: --- --- * @{#RAT.SetTakeoff}("cold"), which means that all aircraft are spawned with their engines off, --- * @{#RAT.SetTakeoff}("hot"), which means that all aircraft are spawned with their engines on, --- * @{#RAT.SetTakeoff}("runway"), which means that all aircraft are spawned already at the runway ready to takeoff. --- Note that in this case the default spawn intervall is set to 180 seconds in order to avoid aircraft jamms on the runway. Generally, this takeoff at runways should be used with care and problems are to be expected. --- --- --- The options @{#RAT.SetMinDistance}() and @{#RAT.SetMaxDistance}() can be used to restrict the range from departure to destination. For example --- --- * @{#RAT.SetMinDistance}(100) will cause only random destination airports to be selected which are **at least** 100 km away from the departure airport. --- * @{#RAT.SetMaxDistance}(150) will allow only destination airports which are **less than** 150 km away from the departure airport. --- --- ![Process](..\Presentations\RAT\RAT_Gaussian.png) --- --- By default planes get a cruise altitude of ~20,000 ft ASL. The actual altitude is sampled from a Gaussian distribution. The picture shows this distribution --- if one would spawn 1000 planes. As can be seen most planes get a cruising alt of around FL200. Other values are possible but less likely the further away --- one gets from the expectation value. --- --- The expectation value, i.e. the altitude most aircraft get, can be set with the function @{#RAT.SetFLcruise}(). --- It is possible to restrict the minimum cruise altitude by @{#RAT.SetFLmin}() and the maximum cruise altitude by @{#RAT.SetFLmax}() --- --- The cruise altitude can also be given in meters ASL by the functions @{#RAT.SetCruiseAltitude}(), @{#RAT.SetMinCruiseAltitude}() and @{#RAT.SetMaxCruiseAltitude}(). --- --- For example: --- --- * @{#RAT.SetFLcruise}(300) will cause most planes fly around FL300. --- * @{#RAT.SetFLmin}(100) restricts the cruising alt such that no plane will fly below FL100. Note that this automatically changes the minimum distance from departure to destination. --- That means that only destinations are possible for which the aircraft has had enought time to reach that flight level and descent again. --- * @{#RAT.SetFLmax}(200) will restrict the cruise alt to maximum FL200, i.e. no aircraft will travel above this height. --- --- --- @field #RAT -RAT={ - ClassName = "RAT", -- Name of class: RAT = Random Air Traffic. - debug=false, -- Turn debug messages on or off. - alias=nil, -- Alias for spawned group. - spawndelay=5, -- Delay time in seconds before first spawning happens. - spawninterval=5, -- Interval between spawning units/groups. Note that we add a randomization of 50%. - coalition = nil, -- Coalition of spawn group template. - country = nil, -- Country of the group template. - category = nil, -- Category of aircarft: "plane" or "heli". - friendly = "same", -- Possible departure/destination airport: all=blue+red+neutral, same=spawn+neutral, spawnonly=spawn, blue=blue+neutral, blueonly=blue, red=red+neutral, redonly=red. - ctable = {}, -- Table with the valid coalitons from choice self.friendly. - aircraft = {}, -- Table which holds the basic aircraft properties (speed, range, ...). - Vcruisemax=nil, -- Max cruise speed in set by user. - Vclimb=1500, -- Default climb rate in ft/min. - AlphaDescent=3.6, -- Default angle of descenti in degrees. A value of 3.6 follows the 3:1 rule of 3 miles of travel and 1000 ft descent. - roe = "hold", -- ROE of spawned groups, default is weapon hold (this is a peaceful class for civil aircraft or ferry missions). Possible: "hold", "return", "free". - rot = "noreaction", -- ROT of spawned groups, default is no reaction. Possible: "noreaction", "passive", "evade". - takeoff = 0, -- Takeoff type. 0=coldorhot. - mindist = 5000, -- Min distance from departure to destination in meters. Default 5 km. - maxdist = 500000, -- Max distance from departure to destination in meters. Default 5000 km. - airports_map={}, -- All airports available on current map (Caucasus, Nevada, Normandy, ...). - airports={}, -- All airports of friedly coalitions. - random_departure=true, -- By default a random friendly airport is chosen as departure. - random_destination=true, -- By default a random friendly airport is chosen as destination. - departure_zones={}, -- Array containing the names of the departure zones. - departure_ports={}, -- Array containing the names of the departure airports. - destination_ports={}, -- Array containing the names of the destination airports. - excluded_ports={}, -- Array containing the names of explicitly excluded airports. - ratcraft={}, -- Array with the spawned RAT aircraft. - Tinactive=300, -- Time in seconds after which inactive units will be destroyed. Default is 300 seconds. - reportstatus=false, -- Aircraft report status. - statusinterval=30, -- Intervall between status checks (and reports if enabled). - placemarkers=false, -- Place markers of waypoints on F10 map. - FLminuser=nil, -- Minimum flight level set by user. - FLmaxuser=nil, -- Maximum flight level set by user. - FLuser=nil, -- Flight level set by users explicitly. - commute=false, -- Aircraft commute between departure and destination, i.e. when respawned the departure airport becomes the new destiation. - continuejourney=false, -- Aircraft will continue their journey, i.e. get respawned at their destination with a new random destination. - alive=0, -- Number of groups which are alive. - ngroups=nil, -- Number of groups to be spawned in total. - f10menu=true, -- Add an F10 menu for RAT. - Menu={}, -- F10 menu items for this RAT object. - SubMenuName=nil, -- Submenu name for RAT object. - respawn_at_landing=false, -- Respawn aircraft the moment they land rather than at engine shutdown. - respawn_delay=nil, -- Delay in seconds until repawn happens after landing. - markerids={}, -- Array with marker IDs. - livery=nil, -- Livery of the aircraft. - skill="High", -- Skill of AI. - ATCswitch=true, -- Enable ATC. -} - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Categories of the RAT class. --- @list cat --- @field #string plane Plane. --- @field #string heli Heli. -RAT.cat={ - plane="plane", - heli="heli", -} - ---- RAT waypoint type. --- @list wp -RAT.wp={ - coldorhot=0, - air=1, - runway=2, - hot=3, - cold=4, - climb=5, - cruise=6, - descent=7, - holding=8, - landing=9, -} - ---- RAT friendly coalitions. --- @list coal -RAT.coal={ - same="same", - sameonly="sameonly", - neutral="neutral", -} - ---- RAT unit conversions. --- @list unit -RAT.unit={ - ft2meter=0.305, - kmh2ms=0.278, - FL2m=30.48, - nm2km=1.852, - nm2m=1852, -} - ---- RAT rules of engagement. --- @list ROE -RAT.ROE={ - weaponhold="hold", - weaponfree="free", - returnfire="return", -} - ---- RAT reaction to threat. --- @list ROT -RAT.ROT={ - evade="evade", - passive="passive", - noreaction="noreaction", -} - -RAT.ATC={ - init=false, - flight={}, - airport={}, - unregistered=-1, - onfinal=-100, -} - ---- Running number of placed markers on the F10 map. --- @field #number markerid -RAT.markerid=0 - ---- Main F10 menu. --- @field #string MenuF10 -RAT.MenuF10=nil - ---- Some ID to identify who we are in output of the DCS.log file. --- @field #string id -RAT.id="RAT | " - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---TODO list: ---DONE: Add scheduled spawn. ---DONE: Add possibility to spawn in air. ---DONE: Add departure zones for air start. ---DONE: Make more functions to adjust/set RAT parameters. ---DONE: Clean up debug messages. ---DONE: Improve flight plan. Especially check FL against route length. ---DONE: Add event handlers. ---DONE: Respawn units when they have landed. ---DONE: Change ROE state. ---DONE: Make ROE state user function ---DONE: Improve status reports. ---DONE: Check compatibility with other #SPAWN functions. nope, not all! ---DONE: Add possibility to continue journey at destination. Need "place" in event data for that. ---DONE: Add enumerators and get rid off error prone string comparisons. ---DONE: Check that FARPS are not used as airbases for planes. ---DONE: Add special cases for ships (similar to FARPs). ---DONE: Add cases for helicopters. ---DONE: Add F10 menu. ---DONE: Add markers to F10 menu. ---DONE: Add respawn limit. Later... ---DONE: Make takeoff method random between cold and hot start. ---DONE: Check out uncontrolled spawning. Not now! ---DONE: Check aircraft spawning in air at Sochi after third aircraft was spawned. ==> DCS behaviour. ---DONE: Improve despawn after stationary. Might lead to despawning if many aircraft spawn at the same time. ---DONE: Check why birth event is not handled. ==> Seems to be okay if it is called _OnBirth rather than _OnBirthday. Dont know why actually!? ---DONE: Improve behaviour when no destination or departure airports were found. Leads to crash, e.g. 1184: attempt to get length of local 'destinations' (a nil value) ---DONE: Check cases where aircraft get shot down. ---DONE: Handle the case where more than 10 RAT objects are spawned. Likewise, more than 10 groups of one object. Causes problems with the number of menu items! ==> not now! ---DONE: Add custom livery choice if possible. ---TODO: When only a destination is set, it should be checked that the departure is within range. Also, that departure and destination are not the same. - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Create a new RAT object. --- @param #RAT self --- @param #string groupname Name of the group as defined in the mission editor. This group is serving as a template for all spawned units. --- @param #string alias (Optional) Alias of the group. This is and optional parameter but must(!) be used if the same template group is used for more than one RAT object. --- @return #RAT Object of RAT class. --- @return #nil If the group does not exist in the mission editor. --- @usage yak1:RAT("RAT_YAK") will create a RAT object called "yak1". The template group in the mission editor must have the name "RAT_YAK". --- @usage yak2:RAT("RAT_YAK", "Yak2") will create a RAT object "yak2". The template group in the mission editor must have the name "RAT_YAK" but the group will be called "Yak2" in e.g. the F10 menu. -function RAT:New(groupname, alias) - - -- Welcome message. - env.info(RAT.id.."Creating new RAT object from template: "..groupname) - - -- Set alias. - alias=alias or groupname - - -- Inherit SPAWN class. - local self=BASE:Inherit(self, SPAWN:NewWithAlias(groupname, alias)) -- #RAT - - -- Alias of groupname. - self.alias=alias - - -- Get template group defined in the mission editor. - local DCSgroup=Group.getByName(groupname) - - -- Check the group actually exists. - if DCSgroup==nil then - env.error("Group with name "..groupname.." does not exist in the mission editor!") - return nil - end - - -- Set own coalition. - self.coalition=DCSgroup:getCoalition() - - -- Initialize aircraft parameters based on ME group template. - self:_InitAircraft(DCSgroup) - - -- Get all airports of current map (Caucasus, NTTR, Normandy, ...). - self:_GetAirportsOfMap() - - return self -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Triggers the spawning of AI aircraft. Note that all additional options should be set before giving the spawn command. --- @param #RAT self --- @param #number naircraft (Optional) Number of aircraft to spawn. Default is one aircraft. --- @usage yak:Spawn(5) will spawn five aircraft. By default aircraft will spawn at neutral and red airports if the template group is part of the red coaliton. -function RAT:Spawn(naircraft) - - -- Number of aircraft to spawn. Default is one. - self.ngroups=naircraft or 1 - - -- Init RAT ATC if not already done. - if self.ATCswitch and not RAT.ATC.init then - RAT:_ATCInit(self.airports_map) - end - - -- Create F10 main menu if it does not exists yet. - if self.f10menu and not RAT.MenuF10 then - RAT.MenuF10 = MENU_MISSION:New("RAT") - end - - -- Set the coalition table based on choice of self.coalition and self.friendly. - self:_SetCoalitionTable() - - -- Get all airports of this map beloning to friendly coalition(s). - self:_GetAirportsOfCoalition() - - -- Set submenuname if it has not been set by user. - if not self.SubMenuName then - self.SubMenuName=self.alias - end - - -- debug message - local text=string.format("\n******************************************************\n") - text=text..string.format("Spawning %i aircraft from template %s of type %s.\n", self.ngroups, self.SpawnTemplatePrefix, self.aircraft.type) - text=text..string.format("Alias: %s\n", self.alias) - text=text..string.format("Category: %s\n", self.category) - text=text..string.format("Friendly coalitions: %s\n", self.friendly) - text=text..string.format("Number of airports on map : %i\n", #self.airports_map) - text=text..string.format("Number of friendly airports: %i\n", #self.airports) - text=text..string.format("Min dist to destination: %4.1f\n", self.mindist) - text=text..string.format("Max dist to destination: %4.1f\n", self.maxdist) - text=text..string.format("Takeoff type: %i\n", self.takeoff) - text=text..string.format("Commute: %s\n", tostring(self.commute)) - text=text..string.format("Journey: %s\n", tostring(self.continuejourney)) - text=text..string.format("Spawn delay: %4.1f\n", self.spawndelay) - text=text..string.format("Spawn interval: %4.1f\n", self.spawninterval) - text=text..string.format("Respawn after landing: %s\n", tostring(self.respawn_at_landing)) - text=text..string.format("Respawn delay: %s\n", tostring(self.respawn_delay)) - text=text..string.format("ROE: %s\n", tostring(self.roe)) - text=text..string.format("ROT: %s\n", tostring(self.rot)) - text=text..string.format("Vclimb: %4.1f\n", self.Vclimb) - text=text..string.format("AlphaDescent: %4.2f\n", self.AlphaDescent) - text=text..string.format("Vcruisemax: %s\n", tostring(self.Vcruisemax)) - text=text..string.format("FLuser: %s\n", tostring(self.Fluser)) - text=text..string.format("FLminuser: %s\n", tostring(self.Flminuser)) - text=text..string.format("FLmaxuser: %s\n", tostring(self.Flmaxuser)) - text=text..string.format("Place markers: %s\n", tostring(self.placemarkers)) - text=text..string.format("Report status: %s\n", tostring(self.reportstatus)) - text=text..string.format("Status interval: %4.1f\n", self.statusinterval) - text=text..string.format("Time inactive: %4.1f\n", self.Tinactive) - text=text..string.format("Create F10 menu : %s\n", tostring(self.f10menu)) - text=text..string.format("F10 submenu name: %s\n", self.SubMenuName) - text=text..string.format("ATC enabled : %s\n", tostring(self.ATCswitch)) - text=text..string.format("******************************************************\n") - env.info(RAT.id..text) - - -- Create submenus. - if self.f10menu then - self.Menu[self.SubMenuName]=MENU_MISSION:New(self.SubMenuName, RAT.MenuF10) - self.Menu[self.SubMenuName]["groups"]=MENU_MISSION:New("Groups", self.Menu[self.SubMenuName]) - MENU_MISSION_COMMAND:New("Spawn new group", self.Menu[self.SubMenuName], self._SpawnWithRoute, self) - MENU_MISSION_COMMAND:New("Delete markers", self.Menu[self.SubMenuName], self._DeleteMarkers, self) - MENU_MISSION_COMMAND:New("Status report", self.Menu[self.SubMenuName], self.Status, self, true) - end - - -- Schedule spawning of aircraft. - local Tstart=self.spawndelay - local dt=self.spawninterval - -- Ensure that interval is >= 180 seconds if spawn at runway is chosen. Aircraft need time to takeoff or the runway gets jammed. - if self.takeoff==RAT.wp.runway and not self.random_departure then - dt=math.max(dt, 180) - end - local Tstop=Tstart+dt*(self.ngroups-1) - SCHEDULER:New(nil, self._SpawnWithRoute, {self}, Tstart, dt, 0.0, Tstop) - - -- Status check and report scheduler. - SCHEDULER:New(nil, self.Status, {self}, Tstart+1, self.statusinterval) - - -- Handle events. - self:HandleEvent(EVENTS.Birth, self._OnBirth) - self:HandleEvent(EVENTS.EngineStartup, self._EngineStartup) - self:HandleEvent(EVENTS.Takeoff, self._OnTakeoff) - self:HandleEvent(EVENTS.Land, self._OnLand) - self:HandleEvent(EVENTS.EngineShutdown, self._OnEngineShutdown) - self:HandleEvent(EVENTS.Dead, self._OnDead) - self:HandleEvent(EVENTS.Crash, self._OnCrash) - -- TODO: add hit event? - -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Set the friendly coalitions from which the airports can be used as departure and destination. --- @param #RAT self --- @param #string friendly "same"=own coalition+neutral (default), "sameonly"=own coalition only, "neutral"=all neutral airports. --- Default is "same", so aircraft will use airports of the coalition their spawn template has plus all neutral airports. --- @usage yak:SetCoalition("neutral") will spawn aircraft randomly on all neutral airports. --- @usage yak:SetCoalition("sameonly") will spawn aircraft randomly on airports belonging to the same coalition only as the template. -function RAT:SetCoalition(friendly) - if friendly:lower()=="sameonly" then - self.friendly=RAT.coal.sameonly - elseif friendly:lower()=="neutral" then - self.friendly=RAT.coal.neutral - else - self.friendly=RAT.coal.same - end -end - ---- Set coalition of RAT group. You can make red templates blue and vice versa. --- @param #RAT self --- @param #string color Color of coalition, i.e. "red" or blue". -function RAT:SetCoalitionAircraft(color) - if color:lower()=="blue" then - self.coalition=coalition.side.BLUE - if not self.country then - self.country=country.id.USA - end - elseif color:lower()=="red" then - self.coalition=coalition.side.RED - if not self.country then - self.country=country.id.RUSSIA - end - elseif color:lower()=="neutral" then - self.coalition=coalition.side.NEUTRAL - end -end - ---- Set country of RAT group. This overrules the coalition settings. --- @param #RAT self --- @param #number id DCS country enumerator ID. For example country.id.USA or country.id.RUSSIA. -function RAT:SetCoalition2(id) - self.country=id -end - ---- Set takeoff type. Starting cold at airport, starting hot at airport, starting at runway, starting in the air. --- Default is "takeoff-coldorhot". So there is a 50% chance that the aircraft starts with cold engines and 50% that it starts with hot engines. --- @param #RAT self --- @param #string type Type can be "takeoff-cold" or "cold", "takeoff-hot" or "hot", "takeoff-runway" or "runway", "air". --- @usage RAT:Takeoff("hot") will spawn RAT objects at airports with engines started. --- @usage RAT:Takeoff("cold") will spawn RAT objects at airports with engines off. --- @usage RAT:Takeoff("air") will spawn RAT objects in air over random airports or within pre-defined zones. -function RAT:SetTakeoff(type) - - local _Type - if type:lower()=="takeoff-cold" or type:lower()=="cold" then - _Type=RAT.wp.cold - elseif type:lower()=="takeoff-hot" or type:lower()=="hot" then - _Type=RAT.wp.hot - elseif type:lower()=="takeoff-runway" or type:lower()=="runway" then - _Type=RAT.wp.runway - elseif type:lower()=="air" then - _Type=RAT.wp.air - else - _Type=RAT.wp.coldorhot - end - - self.takeoff=_Type -end - ---- Set possible departure ports. This can be an airport or a zone defined in the mission editor. --- @param #RAT self --- @param #string names Name or table of names of departure airports or zones. --- @usage RAT:SetDeparture("Sochi-Adler") will spawn RAT objects at Sochi-Adler airport. --- @usage RAT:SetDeparture({"Sochi-Adler", "Gudauta"}) will spawn RAT aircraft radomly at Sochi-Adler or Gudauta airport. --- @usage RAT:SetDeparture({"Zone A", "Gudauta"}) will spawn RAT aircraft in air randomly within Zone A, which has to be defined in the mission editor, or within a zone around Gudauta airport. Note that this also requires RAT:takeoff("air") to be set. -function RAT:SetDeparture(names) - - -- Random departure is deactivated now that user specified departure ports. - self.random_departure=false - - if type(names)=="table" then - - -- we did get a table of names - for _,name in pairs(names) do - - if self:_AirportExists(name) then - -- If an airport with this name exists, we put it in the ports array. - table.insert(self.departure_ports, name) - else - -- If it is not an airport, we assume it is a zone. - -- We need the DCS call to determine if it's really a zone. Otherwise code will crash at some point. - local z=trigger.misc.getZone(name) - if z then - table.insert(self.departure_zones, name) - end - end - - end - - elseif type(names)=="string" then - - if self:_AirportExists(names) then - -- If an airport with this name exists, we put it in the ports array. - table.insert(self.departure_ports, names) - else - -- If it is not an airport, we assume it is a zone. - table.insert(self.departure_zones, names) - end - - else - -- error message - env.error("Input parameter must be a string or a table!") - end - -end - ---- Set name of destination airport for the AI aircraft. If no name is given an airport from the friendly coalition(s) is chosen randomly. --- @param #RAT self --- @param #string names Name of the destination airport or table of destination airports. --- @usage RAT:SetDestination("Krymsk") makes all aircraft of this RAT oject fly to Krymsk airport. -function RAT:SetDestination(names) - - -- Random departure is deactivated now that user specified departure ports. - self.random_destination=false - - if type(names)=="table" then - - for _,name in pairs(names) do - if self:_AirportExists(name) then - table.insert(self.destination_ports, name) - else - local text=string.format("Airport %s does not exist on map!", name) - env.error(text) - end - end - - elseif type(names)=="string" then - - if self:_AirportExists(names) then - self.destination_ports={names} - else - local text=string.format("Airport %s does not exist on map!", names) - env.error(text) - end - - else - -- Error message. - env.error("Input parameter must be a string or a table!") - end - -end - ---- Airports, FARPs and ships explicitly excluded as departures and destinations. --- @param #RAT self --- @param #string ports Name or table of names of excluded airports. -function RAT:ExcludedAirports(ports) - if type(ports)=="string" then - self.excluded_ports={ports} - else - self.excluded_ports=ports - end -end - ---- Set livery of aircraft. If more than one livery is specified in a table, the actually used one is chosen randomly from the selection. --- @param #RAT self --- @param #string skins Name of livery or table of names of liveries. -function RAT:Livery(skins) - if type(skins)=="string" then - self.livery={skins} - else - self.livery=skins - end -end - ---- Aircraft will continue their journey from their destination. This means they are respawned at their destination and get a new random destination. --- @param #RAT self --- @param #boolean switch Turn journey on=true or off=false. If no value is given switch=true. -function RAT:ContinueJourney(switch) - switch=switch or true - self.continuejourney=switch -end - ---- Aircraft will commute between their departure and destination airports. --- Note, this option is not available if aircraft are spawned in air since they don't have a valid departure airport to fly back to. --- @param #RAT self --- @param #boolean switch Turn commute on=true or off=false. If no value is given switch=true. -function RAT:Commute(switch) - switch=switch or true - self.commute=switch -end - ---- Set the delay before first group is spawned. Minimum delay is 0.5 seconds. --- @param #RAT self --- @param #number delay Delay in seconds. -function RAT:SetSpawnDelay(delay) - self.spawndelay=math.max(0.5, delay) -end - ---- Set the interval between spawnings of the template group. Minimum interval is 0.5 seconds. --- @param #RAT self --- @param #number interval Interval in seconds. -function RAT:SetSpawnInterval(interval) - self.spawninterval=math.max(0.5, interval) -end - ---- Make aircraft respawn the moment they land rather than at engine shut down. --- @param #RAT self --- @param #number delay (Optional) Delay in seconds until respawn happens after landing. Default is 180 seconds. -function RAT:RespawnAfterLanding(delay) - delay = delay or 180 - self.respawn_at_landing=true - self.respawn_delay=delay -end - ---- Set the time after which inactive groups will be destroyed. Default is 300 seconds. --- @param #RAT self --- @param #number time Time in seconds. -function RAT:TimeDestroyInactive(time) - self.Tinactive=time -end - ---- Set the maximum cruise speed of the aircraft. --- @param #RAT self --- @param #number speed Speed in km/h. -function RAT:SetMaxCruiseSpeed(speed) - -- Convert to m/s. - self.Vcruisemax=speed/3.6 -end - ---- Set the climb rate. Default is 1500 ft/min. This automatically sets the climb angle. --- @param #RAT self --- @param #number rate Climb rate in ft/min. -function RAT:SetClimbRate(rate) - self.Vclimb=rate -end - ---- Set the angle of descent. Default is 3.6 degrees, which corresponds to 3000 ft descent after one mile of travel. --- @param #RAT self --- @param #number angle Angle of descent in degrees. -function RAT:SetDescentAngle(angle) - self.AlphaDescent=angle -end - ---- Set rules of engagement (ROE). Default is weapon hold. This is a peaceful class. --- @param #RAT self --- @param #string roe "hold" = weapon hold, "return" = return fire, "free" = weapons free. -function RAT:SetROE(roe) - if roe=="return" then - self.roe=RAT.ROE.returnfire - elseif roe=="free" then - self.roe=RAT.ROE.weaponfree - else - self.roe=RAT.ROE.weaponhold - end -end - ---- Set reaction to threat (ROT). Default is no reaction, i.e. aircraft will simply ignore all enemies. --- @param #RAT self --- @param #string rot "noreaction" = no reaction to threats, "passive" = passive defence, "evade" = evade enemy attacks. -function RAT:SetROT(rot) - if rot=="passive" then - self.rot=RAT.ROT.passive - elseif rot=="evade" then - self.rot=RAT.ROT.evade - else - self.rot=RAT.ROT.noreaction - end -end - ---- Set the name of the F10 submenu. Default is the name of the template group. --- @param #RAT self --- @param #string name Submenu name. -function RAT:MenuName(name) - self.SubMenuName=tostring(name) -end - ---- Enable ATC, which manages the landing queue for RAT aircraft if they arrive simultaniously at the same airport. --- @param #RAT self --- @param #boolean switch true=enable ATC, false=disable ATC. -function RAT:EnableATC(switch) - self.ATCswitch=switch -end - ---- Set minimum distance between departure and destination. Default is 5 km. --- Minimum distance should not be smaller than maybe ~500 meters to ensure that departure and destination are different. --- @param #RAT self --- @param #number dist Distance in km. -function RAT:SetMinDistance(dist) - -- Distance in meters. Absolute minimum is 500 m. - self.mindist=math.max(500, dist*1000) -end - ---- Set maximum distance between departure and destination. Default is 5000 km but aircarft range is also taken into account automatically. --- @param #RAT self --- @param #number dist Distance in km. -function RAT:SetMaxDistance(dist) - -- Distance in meters. - self.maxdist=dist*1000 -end - ---- Turn debug messages on or off. Default is off. --- @param #RAT self --- @param #boolean switch true turn messages on, false=off. -function RAT:_Debug(switch) - switch = switch or true - self.debug=switch -end - ---- Aircraft report status messages. Default is off. --- @param #RAT self --- @param #boolean switch true=on, false=off. -function RAT:StatusReports(switch) - switch = switch or true - self.reportstatus=switch -end - ---- Place markers of waypoints on the F10 map. Default is off. --- @param #RAT self --- @param #boolean switch true=yes, false=no. -function RAT:PlaceMarkers(switch) - switch = switch or true - self.placemarkers=switch -end - ---- Set flight level. Setting this value will overrule all other logic. Aircraft will try to fly at this height regardless. --- @param #RAT self --- @param #number height FL in hundrets of feet. E.g. FL200 = 20000 ft ASL. -function RAT:SetFL(height) - self.FLuser=height*RAT.unit.FL2m -end - ---- Set max flight level. Setting this value will overrule all other logic. Aircraft will try to fly at less than this FL regardless. --- @param #RAT self --- @param #number height Maximum FL in hundrets of feet. -function RAT:SetFLmax(height) - self.FLmaxuser=height*RAT.unit.FL2m -end - ---- Set max cruising altitude above sea level. --- @param #RAT self --- @param #number alt Altitude ASL in meters. -function RAT:SetMaxCruiseAltitude(alt) - self.FLmaxuser=alt -end - ---- Set min flight level. Setting this value will overrule all other logic. Aircraft will try to fly at higher than this FL regardless. --- @param #RAT self --- @param #number height Maximum FL in hundrets of feet. -function RAT:SetFLmin(height) - self.FLminuser=height*RAT.unit.FL2m -end - ---- Set min cruising altitude above sea level. --- @param #RAT self --- @param #number alt Altitude ASL in meters. -function RAT:SetMinCruiseAltitude(alt) - self.FLminuser=alt -end - ---- Set flight level of cruising part. This is still be checked for consitancy with selected route and prone to radomization. --- Default is FL200 for planes and FL005 for helicopters. --- @param #RAT self --- @param #number height FL in hundrets of feet. E.g. FL200 = 20000 ft ASL. -function RAT:SetFLcruise(height) - self.aircraft.FLcruise=height*RAT.unit.FL2m -end - ---- Set cruising altitude. This is still be checked for consitancy with selected route and prone to radomization. --- @param #RAT self --- @param #number alt Cruising altitude ASL in meters. -function RAT:SetCruiseAltitude(alt) - self.aircraft.FLcruise=alt -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Initialize basic parameters of the aircraft based on its (template) group in the mission editor. --- @param #RAT self --- @param Dcs.DCSWrapper.Group#Group DCSgroup Group of the aircraft in the mission editor. -function RAT:_InitAircraft(DCSgroup) - - local DCSunit=DCSgroup:getUnit(1) - local DCSdesc=DCSunit:getDesc() - local DCScategory=DCSgroup:getCategory() - local DCStype=DCSunit:getTypeName() - - -- Descriptors table of unit. - if self.debug then - self:E({"DCSdesc", DCSdesc}) - end - - -- set category - if DCScategory==Group.Category.AIRPLANE then - self.category=RAT.cat.plane - elseif DCScategory==Group.Category.HELICOPTER then - self.category=RAT.cat.heli - else - self.category="other" - env.error(RAT.id.."Group of RAT is neither airplane nor helicopter!") - end - - -- Get type of aircraft. - self.aircraft.type=DCStype - - -- inital fuel in % - self.aircraft.fuel=DCSunit:getFuel() - - -- operational range in NM converted to m - self.aircraft.Rmax = DCSdesc.range*RAT.unit.nm2m - - -- effective range taking fuel into accound and a 10% reserve - self.aircraft.Reff = self.aircraft.Rmax*self.aircraft.fuel*0.9 - - -- max airspeed from group - self.aircraft.Vmax = DCSdesc.speedMax - - -- max climb speed in m/s - self.aircraft.Vymax=DCSdesc.VyMax - - -- service ceiling in meters - self.aircraft.ceiling=DCSdesc.Hmax - - -- Default flight level (ASL). - if self.category==RAT.cat.plane then - -- For planes: FL200 = 20000 ft = 6096 m. - self.aircraft.FLcruise=200*RAT.unit.FL2m - else - -- For helos: FL005 = 500 ft = 152 m. - self.aircraft.FLcruise=005*RAT.unit.FL2m - end - - -- info message - local text=string.format("\n******************************************************\n") - text=text..string.format("Aircraft parameters:\n") - text=text..string.format("Template group = %s\n", self.SpawnTemplatePrefix) - text=text..string.format("Alias = %s\n", self.alias) - text=text..string.format("Category = %s\n", self.category) - text=text..string.format("Type = %s\n", self.aircraft.type) - text=text..string.format("Max air speed = %6.1f m/s\n", self.aircraft.Vmax) - text=text..string.format("Max climb speed = %6.1f m/s\n", self.aircraft.Vymax) - text=text..string.format("Initial Fuel = %6.1f\n", self.aircraft.fuel*100) - text=text..string.format("Max range = %6.1f km\n", self.aircraft.Rmax/1000) - text=text..string.format("Eff range = %6.1f km\n", self.aircraft.Reff/1000) - text=text..string.format("Ceiling = %6.1f km = FL%3.0f\n", self.aircraft.ceiling/1000, self.aircraft.ceiling/RAT.unit.FL2m) - text=text..string.format("FL cruise = %6.1f km = FL%3.0f\n", self.aircraft.FLcruise/1000, self.aircraft.FLcruise/RAT.unit.FL2m) - text=text..string.format("******************************************************\n") - env.info(RAT.id..text) - -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Spawn the AI aircraft with a route. --- Sets the departure and destination airports and waypoints. --- Modifies the spawn template. --- Sets ROE/ROT. --- Initializes the ratcraft array and group menu. --- @param #RAT self --- @param #string _departure (Optional) Name of departure airbase. --- @param #string _destination (Optional) Name of destination airbase. -function RAT:_SpawnWithRoute(_departure, _destination) - - -- Check that we don't already have more groups than we initally wanted. - --if self.alive > self.ngroups then - -- return - --end - - -- Set takeoff type. - local _takeoff=self.takeoff - if self.takeoff==RAT.wp.coldorhot then - local temp={RAT.wp.cold, RAT.wp.hot} - _takeoff=temp[math.random(2)] - end - - -- Set flight plan. - local departure, destination, waypoints = self:_SetRoute(_takeoff, _departure, _destination) - - -- Return nil if we could not find a departure destination or waypoints - if not (departure and destination and waypoints) then - return nil - end - - -- Modify the spawn template to follow the flight plan. - self:_ModifySpawnTemplate(waypoints) - - -- Actually spawn the group. - local group=self:SpawnWithIndex(self.SpawnIndex) -- Wrapper.Group#GROUP - self.alive=self.alive+1 - - -- ATC is monitoring this flight. - if self.ATCswitch then - RAT:_ATCAddFlight(group:GetName(), destination:GetName()) - end - - -- Set ROE, default is "weapon hold". - self:_SetROE(group, self.roe) - - -- Set ROT, default is "no reaction". - self:_SetROT(group, self.rot) - - -- Init ratcraft array. - self.ratcraft[self.SpawnIndex]={} - self.ratcraft[self.SpawnIndex]["group"]=group - self.ratcraft[self.SpawnIndex]["destination"]=destination - self.ratcraft[self.SpawnIndex]["departure"]=departure - self.ratcraft[self.SpawnIndex]["waypoints"]=waypoints - self.ratcraft[self.SpawnIndex]["status"]="spawned" - self.ratcraft[self.SpawnIndex]["airborne"]=group:InAir() - -- Time and position on ground. For check if aircraft is stuck somewhere. - if group:InAir() then - self.ratcraft[self.SpawnIndex]["Tground"]=nil - self.ratcraft[self.SpawnIndex]["Pground"]=nil - self.ratcraft[self.SpawnIndex]["Tlastcheck"]=nil - else - self.ratcraft[self.SpawnIndex]["Tground"]=timer.getTime() - self.ratcraft[self.SpawnIndex]["Pground"]=group:GetCoordinate() - self.ratcraft[self.SpawnIndex]["Tlastcheck"]=timer.getTime() - end - -- Initial and current position. For calculating the travelled distance. - self.ratcraft[self.SpawnIndex]["P0"]=group:GetCoordinate() - self.ratcraft[self.SpawnIndex]["Pnow"]=group:GetCoordinate() - self.ratcraft[self.SpawnIndex]["Distance"]=0 - - -- Each aircraft gets its own takeoff type. unused? - self.ratcraft[self.SpawnIndex]["takeoff"]=_takeoff - - - -- Create submenu for this group. - if self.f10menu then - local name=self.aircraft.type.." ID "..tostring(self.SpawnIndex) - -- F10/RAT//Group X - self.Menu[self.SubMenuName].groups[self.SpawnIndex]=MENU_MISSION:New(name, self.Menu[self.SubMenuName].groups) - -- F10/RAT//Group X/Set ROE - self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"]=MENU_MISSION:New("Set ROE", self.Menu[self.SubMenuName].groups[self.SpawnIndex]) - MENU_MISSION_COMMAND:New("Weapons hold", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"], self._SetROE, self, group, RAT.ROE.weaponhold) - MENU_MISSION_COMMAND:New("Weapons free", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"], self._SetROE, self, group, RAT.ROE.weaponfree) - MENU_MISSION_COMMAND:New("Return fire", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"], self._SetROE, self, group, RAT.ROE.returnfire) - -- F10/RAT//Group X/Set ROT - self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"]=MENU_MISSION:New("Set ROT", self.Menu[self.SubMenuName].groups[self.SpawnIndex]) - MENU_MISSION_COMMAND:New("No reaction", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.noreaction) - MENU_MISSION_COMMAND:New("Passive defense", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.passive) - MENU_MISSION_COMMAND:New("Evade on fire", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.evade) - -- F10/RAT//Group X/ - MENU_MISSION_COMMAND:New("Despawn group", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self._Despawn, self, group) - MENU_MISSION_COMMAND:New("Clear for landing", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self.ClearForLanding, self, group:GetName()) - MENU_MISSION_COMMAND:New("Place markers", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self._PlaceMarkers, self, waypoints) - MENU_MISSION_COMMAND:New("Status report", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self.Status, self, true, self.SpawnIndex) - end - - return self.SpawnIndex - -end - -function RAT:ClearForLanding(name) - env.info("ATC: setting user flag "..name.." to 1.") - trigger.action.setUserFlag(name, 1) - local flagvalue=trigger.misc.getUserFlag(name) - env.info("ATC: user flag "..name.." ="..flagvalue) -end - ---- Respawn a group. --- @param #RAT self --- @param Wrapper.Group#GROUP group Group to be repawned. -function RAT:_Respawn(group) - - -- Get the spawn index from group - local index=self:GetSpawnIndexFromGroup(group) - - -- Get departure and destination from previous journey. - local departure=self.ratcraft[index].departure - local destination=self.ratcraft[index].destination - - local _departure=nil - local _destination=nil - - if self.continuejourney then - -- We continue our journey from the old departure airport. - _departure=destination:GetName() - elseif self.commute then - -- We commute between departure and destination. - _departure=destination:GetName() - _destination=departure:GetName() - end - - -- Spawn new group. - if self.respawn_delay then - SCHEDULER:New(nil, self._SpawnWithRoute, {self, _departure, _destination}, self.respawn_delay) - else - self:_SpawnWithRoute(_departure, _destination) - end - -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Set the route of the AI plane. Due to DCS landing bug, this has to be done before the unit is spawned. --- @param #RAT self --- @param takeoff #RAT.wp Takeoff type. --- @param Wrapper.Airport#AIRBASE _departure (Optional) Departure airbase. --- @param Wrapper.Airport#AIRBASE _destination (Optional) Destination airbase. --- @return Wrapper.Airport#AIRBASE Departure airbase. --- @return Wrapper.Airport#AIRBASE Destination airbase. --- @return #table Table of flight plan waypoints. --- @return #nil If no valid departure or destination airport could be found. -function RAT:_SetRoute(takeoff, _departure, _destination) - - -- Max cruise speed. - local VxCruiseMax - if self.Vcruisemax then - -- User input. - VxCruiseMax = min(self.Vcruisemax, self.aircraft.Vmax) - else - -- Max cruise speed 90% of Vmax or 900 km/h whichever is lower. - VxCruiseMax = math.min(self.aircraft.Vmax*0.90, 250) - end - - -- Min cruise speed 70% of max cruise or 600 km/h whichever is lower. - local VxCruiseMin = math.min(VxCruiseMax*0.70, 166) - - -- Cruise speed (randomized). Expectation value at midpoint between min and max. - local VxCruise = self:_Random_Gaussian((VxCruiseMax-VxCruiseMin)/2+VxCruiseMin, (VxCruiseMax-VxCruiseMax)/4, VxCruiseMin, VxCruiseMax) - - -- Climb speed 90% ov Vmax but max 720 km/h. - local VxClimb = math.min(self.aircraft.Vmax*0.90, 200) - - -- Descent speed 60% of Vmax but max 500 km/h. - local VxDescent = math.min(self.aircraft.Vmax*0.60, 140) - - -- Holding speed is 90% of descent speed. - local VxHolding = VxDescent*0.9 - - -- Final leg is 90% of holding speed. - local VxFinal = VxHolding*0.9 - - -- Reasonably civil climb speed Vy=1500 ft/min = 7.6 m/s but max aircraft specific climb rate. - local VyClimb=math.min(self.Vclimb*RAT.unit.ft2meter/60, self.aircraft.Vymax) - - -- Climb angle in rad. - local AlphaClimb=math.asin(VyClimb/VxClimb) - - -- Descent angle in rad. - local AlphaDescent=math.rad(self.AlphaDescent) - - - -- DEPARTURE AIRPORT - -- Departure airport or zone. - local departure=nil - if _departure then - if self:_AirportExists(_departure) then - departure=AIRBASE:FindByName(_departure) - else - local text=string.format("ERROR: Specified departure airport %s does not exist for %s!", _departure, self.alias) - env.error(RAT.id..text) - end - else - departure=self:_PickDeparture(takeoff) - end - - -- Return nil if no departure could be found. - if not departure then - local text=string.format("No valid departure airport could be found for %s.", self.alias) - MESSAGE:New(text, 60):ToAll() - env.error(RAT.id..text) - return nil - end - - -- Coordinates of departure point. - local Pdeparture - if takeoff==RAT.wp.air then - -- For an air start, we take a random point within the spawn zone. - local vec2=departure:GetRandomVec2() - --Pdeparture=COORDINATE:New(vec2.x, self.aircraft.FLcruise, vec2.y) - Pdeparture=COORDINATE:NewFromVec2(vec2) - else - Pdeparture=departure:GetCoordinate() - end - - -- Height ASL of departure point. - local H_departure - if takeoff==RAT.wp.air then - -- Departure altitude is 70% of default cruise with 30% variation and limited to 1000 m AGL (50 m for helos). - local Hmin - if self.category==RAT.cat.plane then - Hmin=1000 - else - Hmin=50 - end - H_departure=self:_Randomize(self.aircraft.FLcruise*0.7, 0.3, Pdeparture.y+Hmin, self.aircraft.FLcruise) - else - H_departure=Pdeparture.y - end - - -- Adjust min distance between departure and destination for user set min flight level. - if self.FLminuser then - self.mindist=self:_MinDistance(AlphaClimb, AlphaDescent, self.FLminuser-H_departure) - local text=string.format("Adjusting min distance to %d km (for given min FL%03d)", self.mindist/1000, self.FLminuser/RAT.unit.FL2m) - env.info(RAT.id..text) - end - - -- DESTINATION AIRPORT - local destination=nil - if _destination then - - if self:_AirportExists(_destination) then - destination=AIRBASE:FindByName(_destination) - else - local text=string.format("ERROR: Specified destination airport %s does not exist for %s!", _destination, self.alias) - env.error(RAT.id..text) - end - - else - - -- This handes the case where we have a journey and the first flight is done, i.e. _departure is set. - -- If a user specified more than two destination airport explicitly, then we will stick to this. - -- Otherwise, the route is random from now on. - if self.continuejourney and _departure and #self.destination_ports<3 then - self.random_destination=true - end - - -- Get all destination airports within reach. - local destinations=self:_GetDestinations(departure, Pdeparture, self.mindist, math.min(self.aircraft.Reff, self.maxdist)) - - -- Pick a destination airport. - destination=self:_PickDestination(destinations) - end - - -- Return nil if no departure could be found. - if not destination then - local text=string.format("No valid destination airport could be found for %s!", self.alias) - MESSAGE:New(text, 60):ToAll() - env.error(RAT.id..text) - return nil - end - - -- Check that departure and destination are not the same. Should not happen due to mindist. - if destination:GetName()==departure:GetName() then - local text=string.format("%s: Destination and departure airport are identical. Airport %s (ID %d).", self.alias, destination:GetName(), destination:GetID()) - MESSAGE:New(text, 120):ToAll() - env.error(RAT.id..text) - end - - -- Coordinates of destination airport. - local Pdestination=destination:GetCoordinate() - -- Height ASL of destination airport. - local H_destination=Pdestination.y - - -- DESCENT/HOLDING POINT - -- Get a random point between 5 and 10 km away from the destination. - local Vholding - if self.category==RAT.cat.plane then - Vholding=destination:GetCoordinate():GetRandomVec2InRadius(10000, 5000) - else - -- For helos we set a distance between 500 to 1000 m. - Vholding=destination:GetCoordinate():GetRandomVec2InRadius(1000, 500) - end - -- Coordinates of the holding point. y is the land height at that point. - local Pholding=COORDINATE:NewFromVec2(Vholding) - - -- AGL height of holding point. - local H_holding=Pholding.y - - -- Holding point altitude. For planes between 1600 and 2400 m AGL. For helos 160 to 240 m AGL. - local h_holding - if self.category==RAT.cat.plane then - h_holding=1200 - else - h_holding=150 - end - h_holding=self:_Randomize(h_holding, 0.2) - - -- Distance from holding point to final destination. - local d_holding=Pholding:Get2DDistance(Pdestination) - - -- Height difference between departure and holding point. - local deltaH=math.abs(h_holding+H_holding-H_departure) - - -- GENERAL - -- Heading from departure to holding point of destination. - local heading=self:_Course(Pdeparture, Pholding) - - -- Total distance between departure and holding point near destination. - local d_total=Pdeparture:Get2DDistance(Pholding) - - -- Climb/descent angle from departure to holding point - local phi=math.atan(deltaH/d_total) - - -- Corrected climb angle. - local PhiClimb=AlphaClimb+phi - - -- Corrected descent angle. - local PhiDescent=AlphaDescent-phi - - --CRUISE - - -- Max flight level the aircraft can reach if it only climbs and immidiately descents again (i.e. no cruising part). - local FLmax=self:_FLmax(AlphaClimb, AlphaDescent, d_total, phi, H_departure) - - -- Min cruise alt is just above holding point at destination or departure height, whatever is larger. - local FLmin=math.max(H_departure, H_holding+h_holding) - - -- If the route is very short we set FLmin a bit lower than FLmax. - if FLmin>FLmax then - FLmin=FLmax*0.75 - end - - -- For helicopters we take cruise alt between 50 to 1000 meters above ground. Default cruise alt is ~150 m. - if self.category==RAT.cat.heli then - FLmin=math.max(H_departure, H_destination)+50 - FLmax=math.max(H_departure, H_destination)+1000 - end - - -- Ensure that FLmax not above 90% its service ceiling. - FLmax=math.min(FLmax, self.aircraft.ceiling*0.9) - - -- Overrule setting if user specified min/max flight level explicitly. - if self.FLminuser then - FLmin=self.FLminuser - end - if self.FLmaxuser then - FLmax=self.FLmaxuser - end - - -- Adjust FLcruise to be at leat FLmin and at most FLmax - if self.aircraft.FLcruiseFLmax then - self.aircraft.FLcruise=FLmax - end - - -- Set cruise altitude. Selected from Gaussian distribution but limited to FLmin and FLmax. - local FLcruise=self:_Random_Gaussian(self.aircraft.FLcruise, (FLmax-FLmin)/4, FLmin, FLmax) - - -- Overrule setting if user specified a flight level explicitly. - if self.FLuser then - FLcruise=self.FLuser - end - - -- CLIMB - -- Height of climb relative to ASL height of departure airport. - local h_climb=FLcruise-H_departure - -- x-distance of climb part - local d_climb=h_climb/math.tan(PhiClimb) - - -- DESCENT - -- Height difference for descent form cruise alt to holding point. - local h_descent=FLcruise-(H_holding+h_holding) - -- x-distance of descent part - local d_descent=h_descent/math.tan(PhiDescent) - - -- CRUISE - -- Distance of the cruising part. This should in principle not become negative, but can happen for very short legs. - local d_cruise=d_total-d_climb-d_descent - - -- debug message - local text=string.format("\n******************************************************\n") - text=text..string.format("Template = %s\n\n", self.SpawnTemplatePrefix) - text=text..string.format("Speeds:\n") - text=text..string.format("VxCruiseMin = %6.1f m/s = %5.1f km/h\n", VxCruiseMin, VxCruiseMin*3.6) - text=text..string.format("VxCruiseMax = %6.1f m/s = %5.1f km/h\n", VxCruiseMax, VxCruiseMax*3.6) - text=text..string.format("VxCruise = %6.1f m/s = %5.1f km/h\n", VxCruise, VxCruise*3.6) - text=text..string.format("VxClimb = %6.1f m/s = %5.1f km/h\n", VxClimb, VxClimb*3.6) - text=text..string.format("VxDescent = %6.1f m/s = %5.1f km/h\n", VxDescent, VxDescent*3.6) - text=text..string.format("VxHolding = %6.1f m/s = %5.1f km/h\n", VxHolding, VxHolding*3.6) - text=text..string.format("VxFinal = %6.1f m/s = %5.1f km/h\n", VxFinal, VxFinal*3.6) - text=text..string.format("VyClimb = %6.1f m/s\n", VyClimb) - text=text..string.format("\nDistances:\n") - text=text..string.format("d_climb = %6.1f km\n", d_climb/1000) - text=text..string.format("d_cruise = %6.1f km\n", d_cruise/1000) - text=text..string.format("d_descent = %6.1f km\n", d_descent/1000) - text=text..string.format("d_holding = %6.1f km\n", d_holding/1000) - text=text..string.format("d_total = %6.1f km\n", d_total/1000) - text=text..string.format("\nHeights:\n") - text=text..string.format("H_departure = %6.1f m ASL\n", H_departure) - text=text..string.format("H_destination = %6.1f m ASL\n", H_destination) - text=text..string.format("H_holding = %6.1f m ASL\n", H_holding) - text=text..string.format("h_climb = %6.1f m\n", h_climb) - text=text..string.format("h_descent = %6.1f m\n", h_descent) - text=text..string.format("h_holding = %6.1f m\n", h_holding) - text=text..string.format("delta H = %6.1f m\n", deltaH) - text=text..string.format("FLmin = %6.1f m ASL = FL%03d\n", FLmin, FLmin/RAT.unit.FL2m) - text=text..string.format("FLcruise = %6.1f m ASL = FL%03d\n", FLcruise, FLcruise/RAT.unit.FL2m) - text=text..string.format("FLmax = %6.1f m ASL = FL%03d\n", FLmax, FLmax/RAT.unit.FL2m) - text=text..string.format("\nAngles:\n") - text=text..string.format("Alpha climb = %6.1f Deg\n", math.deg(AlphaClimb)) - text=text..string.format("Alpha descent = %6.1f Deg\n", math.deg(AlphaDescent)) - text=text..string.format("Phi (slope) = %6.1f Deg\n", math.deg(phi)) - text=text..string.format("Phi climb = %6.1f Deg\n", math.deg(PhiClimb)) - text=text..string.format("Phi descent = %6.1f Deg\n", math.deg(PhiDescent)) - text=text..string.format("Heading = %6.1f Deg\n", heading) - text=text..string.format("******************************************************\n") - env.info(RAT.id..text) - - -- Ensure that cruise distance is positve. Can be slightly negative in special cases. And we don't want to turn back. - if d_cruise<0 then - d_cruise=100 - end - - -- Coordinates of route from departure (0) to cruise (1) to descent (2) to holing (3) to destination (4). - local c0=Pdeparture - local c1=c0:Translate(d_climb/2, heading) - local c2=c1:Translate(d_climb/2, heading) - local c3=c2:Translate(d_cruise, heading) - local c4=c3:Translate(d_descent/2, heading) - local c5=Pholding - local c6=Pdestination - - --Convert coordinates into route waypoints. - local wp0=self:_Waypoint(takeoff, c0, VxClimb, H_departure, departure) - local wp1=self:_Waypoint(RAT.wp.climb, c1, VxClimb, H_departure+(FLcruise-H_departure)/2) - local wp2=self:_Waypoint(RAT.wp.cruise, c2, VxCruise, FLcruise) - local wp3=self:_Waypoint(RAT.wp.cruise, c3, VxCruise, FLcruise) - local wp4=self:_Waypoint(RAT.wp.descent, c4, VxDescent, FLcruise-(FLcruise-(h_holding+H_holding))/2) - local wp5=self:_Waypoint(RAT.wp.holding, c5, VxHolding, H_holding+h_holding) - local wp6=self:_Waypoint(RAT.wp.landing, c6, VxFinal, H_destination, destination) - - -- set waypoints - local waypoints = {wp0, wp1, wp2, wp3, wp4, wp5, wp6} - - -- Place markers of waypoints on F10 map. - if self.placemarkers then - self:_PlaceMarkers(waypoints) - end - - -- some info on the route as message - self:_Routeinfo(waypoints, "Waypoint info in set_route:") - - -- return departure, destination and waypoints - return departure, destination, waypoints - -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Set the departure airport of the AI. If no airport name is given explicitly an airport from the coalition is chosen randomly. --- If takeoff style is set to "air", we use zones around the airports or the zones specified by user input. --- @param #RAT self --- @param #number takeoff Takeoff type. --- @return Wrapper.Airbase#AIRBASE Departure airport if spawning at airport. --- @return Core.Zone#ZONE Departure zone if spawning in air. -function RAT:_PickDeparture(takeoff) - - -- Array of possible departure airports or zones. - local departures={} - - if takeoff==RAT.wp.air then - - if self.random_departure then - - -- Air start above a random airport. - for _,airport in pairs(self.airports) do - if not self:_Excluded(airport:GetName()) then - table.insert(departures, airport:GetZone()) - end - end - - else - - -- Put all specified zones in table. - for _,name in pairs(self.departure_zones) do - if not self:_Excluded(name) then - table.insert(departures, ZONE:New(name)) - end - end - -- Put all specified airport zones in table. - for _,name in pairs(self.departure_ports) do - if not self:_Excluded(name) then - table.insert(departures, AIRBASE:FindByName(name):GetZone()) - end - end - - end - - else - - if self.random_departure then - - -- All friendly departure airports. - for _,airport in pairs(self.airports) do - if not self:_Excluded(airport:GetName()) then - table.insert(departures, airport) - end - end - - else - - -- All airports specified by user - for _,name in pairs(self.departure_ports) do - if not self:_Excluded(name) then - table.insert(departures, AIRBASE:FindByName(name)) - end - end - - end - end - - -- Select departure airport or zone. - local departure=departures[math.random(#departures)] - - local text - if departure and departure:GetName() then - if takeoff==RAT.wp.air then - text="Chosen departure zone: "..departure:GetName() - else - text="Chosen departure airport: "..departure:GetName().." (ID "..departure:GetID()..")" - end - env.info(RAT.id..text) - if self.debug then - MESSAGE:New(text, 30):ToAll() - end - else - departure=nil - end - - return departure -end - - ---- Pick destination airport. If no airport name is given an airport from the coalition is chosen randomly. --- @param #RAT self --- @param #table destinations Table with destination airports. --- @param #boolean _random (Optional) Switch to activate a random selection of airports. --- @return Wrapper.Airbase#AIRBASE Destination airport. -function RAT:_PickDestination(destinations, _random) - - --[[ - -- Take destinations from user input. - if not (self.random_destination or _random) then - - destinations=nil - destinations={} - - -- All airports specified by user. - for _,name in pairs(self.destination_ports) do - if not self:_Excluded(name) then - table.insert(destinations, AIRBASE:FindByName(name)) - end - end - - end - ]] - - -- Randomly select one possible destination. - local destination=nil - if destinations and #destinations>0 then - - -- Random selection. - destination=destinations[math.random(#destinations)] -- Wrapper.Airbase#AIRBASE - - -- Debug message. - local text="Chosen destination airport: "..destination:GetName().." (ID "..destination:GetID()..")" - env.info(RAT.id..text) - if self.debug then - MESSAGE:New(text, 30):ToAll() - end - - else - env.error(RAT.id.."No destination airport found.") - end - - return destination -end - - ---- Get all possible destination airports depending on departure position. --- The list is sorted w.r.t. distance to departure position. --- @param #RAT self --- @param Wrapper.Airbase#AIRBASE departure Departure airport or zone. --- @param Core.Point#COORDINATE q Coordinate of the departure point. --- @param #number minrange Minimum range to q in meters. --- @param #number maxrange Maximum range to q in meters. --- @return #table Table with possible destination airports. --- @return #nil If no airports could be found. -function RAT:_GetDestinations(departure, q, minrange, maxrange) - - -- Min/max range to destination. - minrange=minrange or self.mindist - maxrange=maxrange or self.maxdist - - local possible_destinations={} - if self.random_destination then - - -- Airports of friendly coalitions. - for _,airport in pairs(self.airports) do - local name=airport:GetName() - if self:_IsFriendly(name) and not self:_Excluded(name) and name~=departure:GetName() then - - -- Distance from departure to possible destination - local distance=q:Get2DDistance(airport:GetCoordinate()) - - -- Check if distance form departure to destination is within min/max range. - if distance>=minrange and distance<=maxrange then - table.insert(possible_destinations, airport) - end - end - end - - else - - -- Airports specified by user. - for _,name in pairs(self.destination_ports) do - --if self:_IsFriendly(name) and not self:_Excluded(name) and name~=departure:GetName() then - if name~=departure:GetName() then - local airport=AIRBASE:FindByName(name) - --TODO: Maybe here I should check min/max distance as well? But the user explicitly specified the airports... - table.insert(possible_destinations, airport) - end - end - - end - - -- Info message. - env.info(RAT.id.."Number of possible destination airports = "..#possible_destinations) - - if #possible_destinations > 0 then - --- Compare distance of destination airports. - -- @param Core.Point#COORDINATE a Coordinate of point a. - -- @param Core.Point#COORDINATE b Coordinate of point b. - -- @return #list Table sorted by distance. - local function compare(a,b) - local qa=q:Get2DDistance(a:GetCoordinate()) - local qb=q:Get2DDistance(b:GetCoordinate()) - return qa < qb - end - table.sort(possible_destinations, compare) - else - env.error(RAT.id.."No possible destination airports found!") - possible_destinations=nil - end - - -- Return table with destination airports. - return possible_destinations - -end - ---- Check if airport is excluded from possible departures and destinations. --- @param #RAT self --- @param #string port Name of airport, FARP or ship to check. --- @return #boolean true if airport is excluded and false otherwise. -function RAT:_Excluded(port) - for _,name in pairs(self.excluded_ports) do - if name==port then - return true - end - end - return false -end - ---- Check if airport is friendly, i.e. belongs to the right coalition. --- @param #RAT self --- @param #string port Name of airport, FARP or ship to check. --- @return #boolean true if airport is friendly and false otherwise. -function RAT:_IsFriendly(port) - for _,airport in pairs(self.airports) do - local name=airport:GetName() - if name==port then - return true - end - end - return false -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Get all airports of the current map. --- @param #RAT self -function RAT:_GetAirportsOfMap() - local _coalition - - for i=0,2 do -- cycle coalition.side 0=NEUTRAL, 1=RED, 2=BLUE - - -- set coalition - if i==0 then - _coalition=coalition.side.NEUTRAL - elseif i==1 then - _coalition=coalition.side.RED - elseif i==2 then - _coalition=coalition.side.BLUE - end - - -- get airbases of coalition - local ab=coalition.getAirbases(i) - - -- loop over airbases and put them in a table - for _,airbase in pairs(ab) do - - local _id=airbase:getID() - local _p=airbase:getPosition().p - local _name=airbase:getName() - local _myab=AIRBASE:FindByName(_name) - - -- Add airport to table. - table.insert(self.airports_map, _myab) - - if self.debug then - local text1="MOOSE: Airport ID = ".._myab:GetID().." and Name = ".._myab:GetName()..", Category = ".._myab:GetCategory()..", TypeName = ".._myab:GetTypeName() - local text2="DCS : Airport ID = "..airbase:getID().." and Name = "..airbase:getName()..", Category = "..airbase:getCategory()..", TypeName = "..airbase:getTypeName() - env.info(RAT.id..text1) - env.info(RAT.id..text2) - end - - end - - end -end - ---- Get all "friendly" airports of the current map. --- @param #RAT self -function RAT:_GetAirportsOfCoalition() - for _,coalition in pairs(self.ctable) do - for _,airport in pairs(self.airports_map) do - if airport:GetCoalition()==coalition then - -- Planes cannot land on FARPs. - local condition1=self.category==RAT.cat.plane and airport:GetTypeName()=="FARP" - -- Planes cannot land on ships. - local condition2=self.category==RAT.cat.plane and airport:GetCategory()==1 - if not (condition1 or condition2) then - table.insert(self.airports, airport) - end - end - end - end - - if #self.airports==0 then - local text="No possible departure/destination airports found!" - MESSAGE:New(text, 60):ToAll() - env.error(RAT.id..text) - end -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Report status of RAT groups. --- @param #RAT self --- @param #boolean message (Optional) Send message with report to all if true. --- @param #number forID (Optional) Send message only for this ID. -function RAT:Status(message, forID) - - message=message or false - forID=forID or false - - -- number of ratcraft spawned. - local ngroups=#self.ratcraft - - if (message and not forID) or self.reportstatus then - local text=string.format("Alive groups of template %s: %d", self.SpawnTemplatePrefix, self.alive) - env.info(RAT.id..text) - MESSAGE:New(text, 20):ToAll() - end - - -- Current time. - local Tnow=timer.getTime() - - for i=1, ngroups do - - if self.ratcraft[i].group then - if self.ratcraft[i].group:IsAlive() then - - -- Gather some information. - local group=self.ratcraft[i].group --Wrapper.Group#GROUP - local prefix=self:_GetPrefixFromGroup(group) - local life=self:_GetLife(group) - local fuel=group:GetFuel()*100.0 - local airborne=group:InAir() - local coords=group:GetCoordinate() - local alt=coords.y - --local vel=group:GetVelocityKMH() - local departure=self.ratcraft[i].departure:GetName() - local destination=self.ratcraft[i].destination:GetName() - local type=self.aircraft.type - - - -- Monitor time and distance on ground. - local Tg=0 - local Dg=0 - local dTlast=0 - local stationary=false --lets assume, we did move - if airborne then - -- Aircraft is airborne. - self.ratcraft[i]["Tground"]=nil - self.ratcraft[i]["Pground"]=nil - self.ratcraft[i]["Tlastcheck"]=nil - else - --Aircraft is on ground. - if self.ratcraft[i]["Tground"] then - -- Aircraft was already on ground. Calculate total time on ground. - Tg=Tnow-self.ratcraft[i]["Tground"] - - -- Distance on ground since last check. - Dg=coords:Get2DDistance(self.ratcraft[i]["Pground"]) - - -- Time interval since last check. - dTlast=Tnow-self.ratcraft[i]["Tlastcheck"] - - -- If more than Tinactive seconds passed since last check ==> check how much we moved meanwhile. - if dTlast > self.Tinactive then - - -- If aircraft did not move more than 50 m since last check, we call it stationary and despawn it. - --TODO: add case that the aircraft are currently starting their engines. This should not count as being stationary. - --local starting_engines=self.ratcraft[i].status=="" - if Dg<50 then - stationary=true - end - - -- Set the current time to know when the next check is necessary. - self.ratcraft[i]["Tlastcheck"]=Tnow - self.ratcraft[i]["Pground"]=coords - end - - else - -- First time we see that the aircraft is on ground. Initialize the times and position. - self.ratcraft[i]["Tground"]=Tnow - self.ratcraft[i]["Tlastcheck"]=Tnow - self.ratcraft[i]["Pground"]=coords - end - end - - -- Monitor travelled distance since last check. - local Pn=coords - local Dtravel=Pn:Get2DDistance(self.ratcraft[i]["Pnow"]) - self.ratcraft[i]["Pnow"]=Pn - - -- Add up the travelled distance. - self.ratcraft[i]["Distance"]=self.ratcraft[i]["Distance"]+Dtravel - - -- Distance remaining to destination. - local Ddestination=Pn:Get2DDistance(self.ratcraft[i].destination:GetCoordinate()) - - -- Distance remaining to holding point, which is waypoint 6 - local Hp=COORDINATE:New(self.ratcraft[i].waypoints[6].x, self.ratcraft[i].waypoints[6].alt, self.ratcraft[i].waypoints[6].y) - local Dholding=Pn:Get2DDistance(Hp) - - -- Status shortcut. - local status=self.ratcraft[i].status - - -- Range from holding point - local DRholding - if self.category==RAT.cat.plane then - DRholding=8000 - else - DRholding=2000 - end - - -- If distance to holding point is less then 6 km we register the plane. - if self.ATCswitch and Dholding<=DRholding and string.match(status, "On journey") then - RAT:_ATCRegisterFlight(group:GetName(), Tnow) - self.ratcraft[i].status="Holding" - end - - -- Status report. - if (forID and i==forID) or (not forID) then - local text=string.format("ID %i of group %s\n", i, prefix) - if self.commute then - text=text..string.format("%s commuting between %s and %s\n", type, departure, destination) - elseif self.continuejourney then - text=text..string.format("%s travelling from %s to %s (and continueing form there)\n", type, departure, destination) - else - text=text..string.format("%s travelling from %s to %s\n", type, departure, destination) - end - text=text..string.format("Status: %s", self.ratcraft[i].status) - if airborne then - text=text.." [airborne]\n" - else - text=text.." [on ground]\n" - end - text=text..string.format("Fuel = %3.0f %%\n", fuel) - text=text..string.format("Life = %3.0f %%\n", life) - text=text..string.format("FL%03d = %i m\n", alt/RAT.unit.FL2m, alt) - --text=text..string.format("Speed = %i km/h\n", vel) - text=text..string.format("Distance travelled = %6.1f km\n", self.ratcraft[i]["Distance"]/1000) - --text=text..string.format("Distance to destination = %6.1f km\n", Ddestination/1000) - text=text..string.format("Distance to destination = %6.1f km", Dholding/1000) - if not airborne then - text=text..string.format("\nTime on ground = %6.0f seconds\n", Tg) - text=text..string.format("Position change = %8.1f m since %3.0f seconds.", Dg, dTlast) - end - if self.debug then - env.info(RAT.id..text) - end - if self.reportstatus or message then - MESSAGE:New(text, 20):ToAll() - end - end - -- Despawn groups if they are on ground and don't move or are damaged. - if not airborne then - - -- Despawn unit if it did not move more then 50 m in the last 180 seconds. - if stationary then - local text=string.format("Group %s is despawned after being %4.0f seconds inaktive on ground.", self.SpawnTemplatePrefix, dTlast) - env.info(RAT.id..text) - self:_Despawn(group) - end - -- Despawn group if life is < 10% and distance travelled < 100 m. - if life<10 and Dtravel<100 then - local text=string.format("Damaged group %s is despawned. Life = %3.0f", self.SpawnTemplatePrefix, life) - self:_Despawn(group) - end - - end - end - else - if self.debug then - local text=string.format("Group %i does not exist.", i) - env.info(RAT.id..text) - end - end - end -end - ---- Get (relative) life of first unit of a group. --- @param #RAT self --- @param Wrapper.Group#GROUP group Group of unit. --- @return #number Life of unit in percent. -function RAT:_GetLife(group) - local life=0.0 - if group and group:IsAlive() then - local unit=group:GetUnit(1) - if unit then - life=unit:GetLife()/unit:GetLife0()*100 - else - if self.debug then - env.error(RAT.id.."Unit does not exist in RAT_Getlife(). Returning zero.") - end - end - else - if self.debug then - env.error(RAT.id.."Group does not exist in RAT_Getlife(). Returning zero.") - end - end - return life -end - ---- Set status of group. --- @param #RAT self -function RAT:_SetStatus(group, status) - local index=self:GetSpawnIndexFromGroup(group) - env.info(RAT.id.."Status for group "..group:GetName()..": "..status) - self.ratcraft[index].status=status -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Function is executed when a unit is spawned. --- @param #RAT self -function RAT:_OnBirth(EventData) - - local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP - - if SpawnGroup then - - -- Get the template name of the group. This can be nil if this was not a spawned group. - local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - - if EventPrefix then - - -- Check that the template name actually belongs to this object. - if EventPrefix == self.alias then - - local text="Event: Group "..SpawnGroup:GetName().." was born." - env.info(RAT.id..text) - - -- Set status. - local status - if SpawnGroup:InAir() then - status="Just born (after air start)" - else - status="Starting engines (after birth)" - end - self:_SetStatus(SpawnGroup, status) - - end - end - else - if self.debug then - env.error("Group does not exist in RAT:_OnBirthDay().") - end - end -end - ---- Function is executed when a unit starts its engines. --- @param #RAT self -function RAT:_EngineStartup(EventData) - - local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP - - if SpawnGroup then - - -- Get the template name of the group. This can be nil if this was not a spawned group. - local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - - if EventPrefix then - - -- Check that the template name actually belongs to this object. - if EventPrefix == self.alias then - - local text="Event: Group "..SpawnGroup:GetName().." started engines." - env.info(RAT.id..text) - - -- Set status. - local status - if SpawnGroup:InAir() then - status="On journey (after air start)" - else - status="Taxiing (after engines started)" - end - self:_SetStatus(SpawnGroup, status) - - end - end - - else - if self.debug then - env.error("Group does not exist in RAT:_EngineStartup().") - end - end -end - ---- Function is executed when a unit takes off. --- @param #RAT self -function RAT:_OnTakeoff(EventData) - - local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP - - if SpawnGroup then - - -- Get the template name of the group. This can be nil if this was not a spawned group. - local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - - if EventPrefix then - - -- Check that the template name actually belongs to this object. - if EventPrefix == self.alias then - - local text="Event: Group "..SpawnGroup:GetName().." is airborne." - env.info(RAT.id..text) - - -- Set status. - self:_SetStatus(SpawnGroup, "On journey (after takeoff)") - - end - end - - else - if self.debug then - env.error("Group does not exist in RAT:_OnTakeoff().") - end - end -end - ---- Function is executed when a unit lands. --- @param #RAT self -function RAT:_OnLand(EventData) - - local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP - - if SpawnGroup then - - -- Get the template name of the group. This can be nil if this was not a spawned group. - local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - - if EventPrefix then - - -- Check that the template name actually belongs to this object. - if EventPrefix == self.alias then - - local text="Event: Group "..SpawnGroup:GetName().." landed." - env.info(RAT.id..text) - - -- Set status. - self:_SetStatus(SpawnGroup, "Taxiing (after landing)") - - -- ATC plane landed. Take it out of the queue and set runway to free. - if self.ATCswitch then - RAT:_ATCFlightLanded(SpawnGroup:GetName()) - end - - if self.respawn_at_landing then - text="Event: Group "..SpawnGroup:GetName().." will be respawned." - env.info(RAT.id..text) - - -- Respawn group. - self:_Respawn(SpawnGroup) - end - - end - end - - else - if self.debug then - env.error("Group does not exist in RAT:_OnLand().") - end - end -end - ---- Function is executed when a unit shuts down its engines. --- @param #RAT self -function RAT:_OnEngineShutdown(EventData) - - local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP - - if SpawnGroup then - - -- Get the template name of the group. This can be nil if this was not a spawned group. - local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - - if EventPrefix then - - -- Check that the template name actually belongs to this object. - if EventPrefix == self.alias then - - local text="Event: Group "..SpawnGroup:GetName().." shut down its engines." - env.info(RAT.id..text) - - -- Set status. - self:_SetStatus(SpawnGroup, "Parking (shutting down engines)") - - if not self.respawn_at_landing then - text="Event: Group "..SpawnGroup:GetName().." will be respawned." - env.info(RAT.id..text) - - -- Respawn group. - self:_Respawn(SpawnGroup) - end - - -- Despawn group. - text="Event: Group "..SpawnGroup:GetName().." will be destroyed now." - env.info(RAT.id..text) - self:_Despawn(SpawnGroup) - - end - end - - else - if self.debug then - env.error("Group does not exist in RAT:_OnEngineShutdown().") - end - end -end - ---- Function is executed when a unit is dead. --- @param #RAT self -function RAT:_OnDead(EventData) - - local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP - - if SpawnGroup then - - -- Get the template name of the group. This can be nil if this was not a spawned group. - local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - - if EventPrefix then - - -- Check that the template name actually belongs to this object. - if EventPrefix == self.alias then - - local text="Event: Group "..SpawnGroup:GetName().." died." - env.info(RAT.id..text) - - -- Set status. - self:_SetStatus(SpawnGroup, "Destroyed (after dead)") - - end - end - - else - if self.debug then - env.error("Group does not exist in RAT:_OnDead().") - end - end -end - ---- Function is executed when a unit crashes. --- @param #RAT self -function RAT:_OnCrash(EventData) - - local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP - - env.info(string.format("%sGroup %s crashed!", RAT.id, SpawnGroup:GetName())) - - if SpawnGroup then - - -- Get the template name of the group. This can be nil if this was not a spawned group. - local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - - if EventPrefix then - - -- Check that the template name actually belongs to this object. - if EventPrefix == self.alias then - - local text="Event: Group "..SpawnGroup:GetName().." crashed." - env.info(RAT.id..text) - - -- Set status. - self:_SetStatus(SpawnGroup, "Crashed") - - --TODO: Aircraft are not respawned if they crash. Should they? - - --TODO: Maybe spawn some people at the crash site and send a distress call. - -- And define them as cargo which can be rescued. - end - end - - else - if self.debug then - env.error("Group does not exist in RAT:_OnCrash().") - end - end -end - ---- Despawn unit. Unit gets destoyed and group is set to nil. --- Index of ratcraft array is taken from spawned group name. --- @param #RAT self --- @param Wrapper.Group#GROUP group Group to be despawned. -function RAT:_Despawn(group) - - local index=self:GetSpawnIndexFromGroup(group) - self.ratcraft[index].group:Destroy() - self.ratcraft[index].group=nil - - -- Decrease group alive counter. - self.alive=self.alive-1 - - -- Remove submenu for this group. - if self.f10menu then - self.Menu[self.SubMenuName]["groups"][index]:Remove() - end - - --TODO: Maybe here could be some more arrays deleted? -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Create a waypoint that can be used with the Route command. --- @param #RAT self --- @param #number Type Type of waypoint. --- @param Core.Point#COORDINATE Coord 3D coordinate of the waypoint. --- @param #number Speed Speed in m/s. --- @param #number Altitude Altitude in m. --- @param Wrapper.Airbase#AIRBASE Airport Airport of object to spawn. --- @return #table Waypoints for DCS task route or spawn template. -function RAT:_Waypoint(Type, Coord, Speed, Altitude, Airport) - - -- Altitude of input parameter or y-component of 3D-coordinate. - local _Altitude=Altitude or Coord.y - - -- Land height at given coordinate. - local Hland=Coord:GetLandHeight() - - -- convert type and action in DCS format - local _Type=nil - local _Action=nil - local _alttype="RADIO" - local _AID=nil - - if Type==RAT.wp.cold then - -- take-off with engine off - _Type="TakeOffParking" - _Action="From Parking Area" - _Altitude = 0 - _alttype="RADIO" - _AID = Airport:GetID() - elseif Type==RAT.wp.hot then - -- take-off with engine on - _Type="TakeOffParkingHot" - _Action="From Parking Area Hot" - _Altitude = 0 - _alttype="RADIO" - _AID = Airport:GetID() - elseif Type==RAT.wp.runway then - -- take-off from runway - _Type="TakeOff" - _Action="From Parking Area" - _Altitude = 0 - _alttype="RADIO" - _AID = Airport:GetID() - elseif Type==RAT.wp.air then - -- air start - _Type="Turning Point" - _Action="Turning Point" - _alttype="BARO" - elseif Type==RAT.wp.climb then - _Type="Turning Point" - _Action="Turning Point" - --_Action="Fly Over Point" - _alttype="BARO" - elseif Type==RAT.wp.cruise then - _Type="Turning Point" - _Action="Turning Point" - --_Action="Fly Over Point" - _alttype="BARO" - elseif Type==RAT.wp.descent then - _Type="Turning Point" - _Action="Turning Point" - --_Action="Fly Over Point" - _alttype="BARO" - elseif Type==RAT.wp.holding then - _Type="Turning Point" - _Action="Turning Point" - --_Action="Fly Over Point" - _alttype="BARO" - elseif Type==RAT.wp.landing then - _Type="Land" - _Action="Landing" - _Altitude = 0 - _alttype="RADIO" - _AID = Airport:GetID() - else - env.error("Unknown waypoint type in RAT:Waypoint() function!") - _Type="Turning Point" - _Action="Turning Point" - _alttype="RADIO" - end - - -- some debug info about input parameters - local text=string.format("\n******************************************************\n") - text=text..string.format("Template = %s\n", self.SpawnTemplatePrefix) - text=text..string.format("Type: %i - %s\n", Type, _Type) - text=text..string.format("Action: %s\n", _Action) - text=text..string.format("Coord: x = %6.1f km, y = %6.1f km, alt = %6.1f m\n", Coord.x/1000, Coord.z/1000, Coord.y) - text=text..string.format("Speed = %6.1f m/s = %6.1f km/h = %6.1f knots\n", Speed, Speed*3.6, Speed*1.94384) - text=text..string.format("Land = %6.1f m ASL\n", Hland) - text=text..string.format("Altitude = %6.1f m (%s)\n", _Altitude, _alttype) - if Airport then - if Type==RAT.wp.air then - text=text..string.format("Zone = %s\n", Airport:GetName()) - else - text=text..string.format("Airport = %s with ID %i\n", Airport:GetName(), Airport:GetID()) - end - else - text=text..string.format("No airport/zone specified\n") - end - text=text.."******************************************************\n" - if self.debug then - env.info(RAT.id..text) - end - - -- define waypoint - local RoutePoint = {} - -- coordinates and altitude - RoutePoint.x = Coord.x - RoutePoint.y = Coord.z - RoutePoint.alt = _Altitude - -- altitude type: BARO=ASL or RADIO=AGL - RoutePoint.alt_type = _alttype - -- type - RoutePoint.type = _Type - RoutePoint.action = _Action - -- speed in m/s - RoutePoint.speed = Speed - RoutePoint.speed_locked = true - -- ETA (not used) - RoutePoint.ETA=nil - RoutePoint.ETA_locked = false - -- waypoint name (only for the mission editor) - RoutePoint.name="RAT waypoint" - - if (Airport~=nil) and Type~=RAT.wp.air then - local AirbaseID = Airport:GetID() - local AirbaseCategory = Airport:GetDesc().category - if AirbaseCategory == Airbase.Category.SHIP then - RoutePoint.linkUnit = AirbaseID - RoutePoint.helipadId = AirbaseID - --env.info(RAT.id.."WP: Ship id = "..AirbaseID) - elseif AirbaseCategory == Airbase.Category.HELIPAD then - RoutePoint.linkUnit = AirbaseID - RoutePoint.helipadId = AirbaseID - --env.info(RAT.id.."WP: Helipad id = "..AirbaseID) - elseif AirbaseCategory == Airbase.Category.AIRDROME then - RoutePoint.airdromeId = AirbaseID - --env.info(RAT.id.."WP: Airdrome id = "..AirbaseID) - else - --env.error(RAT.id.."Unknown Airport categoryin _Waypoint()!") - end - end --- if _AID then --- RoutePoint.airdromeId=_AID --- end - -- properties - RoutePoint.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - -- task - if Type==RAT.wp.holding then - -- Duration of holing. Between 10 and 170 seconds. - local Duration=self:_Randomize(90,0.9) - RoutePoint.task=self:_TaskHolding({x=Coord.x, y=Coord.z}, Altitude, Speed, Duration) - else - RoutePoint.task = {} - RoutePoint.task.id = "ComboTask" - RoutePoint.task.params = {} - RoutePoint.task.params.tasks = {} - end - - -- Return waypoint. - return RoutePoint -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Provide information about the assigned flightplan. --- @param #RAT self --- @param #table waypoints Waypoints of the flight plan. --- @param #string comment Some comment to identify the provided information. --- @return #number total Total route length in meters. -function RAT:_Routeinfo(waypoints, comment) - - local text=string.format("\n******************************************************\n") - text=text..string.format("Template = %s\n", self.SpawnTemplatePrefix) - if comment then - text=text..comment.."\n" - end - text=text..string.format("Number of waypoints = %i\n", #waypoints) - -- info on coordinate and altitude - for i=1,#waypoints do - local p=waypoints[i] - text=text..string.format("WP #%i: x = %6.1f km, y = %6.1f km, alt = %6.1f m\n", i-1, p.x/1000, p.y/1000, p.alt) - end - -- info on distance between waypoints - local total=0.0 - for i=1,#waypoints-1 do - local point1=waypoints[i] - local point2=waypoints[i+1] - local x1=point1.x - local y1=point1.y - local x2=point2.x - local y2=point2.y - local d=math.sqrt((x1-x2)^2 + (y1-y2)^2) - local heading=self:_Course(point1, point2) - total=total+d - text=text..string.format("Distance from WP %i-->%i = %6.1f km. Heading = %i.\n", i-1, i, d/1000, heading) - end - text=text..string.format("Total distance = %6.1f km\n", total/1000) - local text=string.format("******************************************************\n") - - -- send message - if self.debug then - env.info(RAT.id..text) - end - - -- return total route length in meters - return total -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Orbit at a specified position at a specified alititude with a specified speed. --- @param #RAT self --- @param Dcs.DCSTypes#Vec2 P1 The point to hold the position. --- @param #number Altitude The altitude ASL at which to hold the position. --- @param #number Speed The speed flying when holding the position in m/s. --- @param #number Duration Duration of holding pattern in seconds. --- @return Dcs.DCSTasking.Task#Task DCSTask -function RAT:_TaskHolding(P1, Altitude, Speed, Duration) - - --local LandHeight = land.getHeight(P1) - - --TODO: randomize P1 - -- Second point is 3 km north of P1 and 200 m for helos. - local dx=3000 - local dy=0 - if self.category==RAT.cat.heli then - dx=200 - dy=0 - end - - local P2={} - P2.x=P1.x+dx - P2.y=P1.y+dy - local Task = { - id = 'Orbit', - params = { - pattern = AI.Task.OrbitPattern.RACE_TRACK, - --pattern = AI.Task.OrbitPattern.CIRCLE, - point = P1, - point2 = P2, - speed = Speed, - altitude = Altitude - } - } - - local DCSTask={} - DCSTask.id="ControlledTask" - DCSTask.params={} - DCSTask.params.task=Task - - if self.ATCswitch then - -- Set stop condition for holding. Either flag=1 or after max. 30 min holding. - local userflagname=string.format("%s#%03d", self.alias, self.SpawnIndex+1) - DCSTask.params.stopCondition={userFlag=userflagname, userFlagValue=1, duration=1800} - else - DCSTask.params.stopCondition={duration=Duration} - end - - return DCSTask -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Calculate the max flight level for a given distance and fixed climb and descent rates. --- In other words we have a distance between two airports and want to know how high we --- can climb before we must descent again to arrive at the destination without any level/cruising part. --- @param #RAT self --- @param #number alpha Angle of climb [rad]. --- @param #number beta Angle of descent [rad]. --- @param #number d Distance between the two airports [m]. --- @param #number phi Angle between departure and destination [rad]. --- @param #number h0 Height [m] of departure airport. Note we implicitly assume that the height difference between departure and destination is negligible. --- @return #number Maximal flight level in meters. -function RAT:_FLmax(alpha, beta, d, phi, h0) --- Solve ASA triangle for one side (d) and two adjacent angles (alpha, beta) given. - local gamma=math.rad(180)-alpha-beta - local a=d*math.sin(alpha)/math.sin(gamma) - local b=d*math.sin(beta)/math.sin(gamma) - -- h1 and h2 should be equal. - local h1=b*math.sin(alpha) - local h2=a*math.sin(beta) - -- We also take the slope between departure and destination into account. - local h3=b*math.cos(math.pi/2-(alpha+phi)) - -- Debug message. - local text=string.format("\nFLmax = FL%3.0f = %6.1f m.\n", h1/RAT.unit.FL2m, h1) - text=text..string.format( "FLmax = FL%3.0f = %6.1f m.\n", h2/RAT.unit.FL2m, h2) - text=text..string.format( "FLmax = FL%3.0f = %6.1f m.", h3/RAT.unit.FL2m, h3) - if self.debug then - env.info(RAT.id..text) - end - return h3+h0 -end - ---- Calculate min distance between departure and destination for given minimum flight level and climb/decent rates --- @param #RAT self --- @param #number alpha Angle of climb [rad]. --- @param #number beta Angle of descent [rad]. --- @param #number h min height AGL. --- @return #number Minimum distance between departure and destiantion. -function RAT:_MinDistance(alpha, beta, h) - local d1=h/math.tan(alpha) - local d2=h/math.tan(beta) - return d1+d2 -end - - ---- Test if an airport exists on the current map. --- @param #RAT self --- @param #string name --- @return #boolean True if airport exsits, false otherwise. -function RAT:_AirportExists(name) - for _,airport in pairs(self.airports_map) do - if airport:GetName()==name then - return true - end - end - return false -end - - ---- Set ROE for a group. --- @param #RAT self --- @param Wrapper.Group#GROUP group Group for which the ROE is set. --- @param #string roe ROE of group. -function RAT:_SetROE(group, roe) - env.info(RAT.id.."Setting ROE to "..roe.." for group "..group:GetName()) - if self.roe==RAT.ROE.returnfire then - group:OptionROEReturnFire() - elseif self.roe==RAT.ROE.weaponfree then - group:OptionROEWeaponFree() - else - group:OptionROEHoldFire() - end -end - - ---- Set ROT for a group. --- @param #RAT self --- @param Wrapper.Group#GROUP group Group for which the ROT is set. --- @param #string rot ROT of group. -function RAT:_SetROT(group, rot) - env.info(RAT.id.."Setting ROT to "..rot.." for group "..group:GetName()) - if self.rot==RAT.ROT.passive then - group:OptionROTPassiveDefense() - elseif self.rot==RAT.ROT.evade then - group:OptionROTEvadeFire() - else - group:OptionROTNoReaction() - end -end - - ---- Create a table with the valid coalitions for departure and destination airports. --- @param #RAT self -function RAT:_SetCoalitionTable() - -- get all possible departures/destinations depending on coalition - if self.friendly==RAT.coal.neutral then - self.ctable={coalition.side.NEUTRAL} - elseif self.friendly==RAT.coal.same then - self.ctable={self.coalition, coalition.side.NEUTRAL} - elseif self.friendly==RAT.coal.sameonly then - self.ctable={self.coalition} - else - env.error("Unknown friendly coalition in _SetCoalitionTable(). Defaulting to NEUTRAL.") - self.ctable={self.coalition, coalition.side.NEUTRAL} - end -end - - ----Determine the heading from point a to point b. ---@param #RAT self ---@param Core.Point#COORDINATE a Point from. ---@param Core.Point#COORDINATE b Point to. ---@return #number Heading/angle in degrees. -function RAT:_Course(a,b) - local dx = b.x-a.x - -- take the right value for y-coordinate (if we have "alt" then "y" if not "z") - local ay - if a.alt then - ay=a.y - else - ay=a.z - end - local by - if b.alt then - by=b.y - else - by=b.z - end - local dy = by-ay - local angle = math.deg(math.atan2(dy,dx)) - if angle < 0 then - angle = 360 + angle - end - return angle -end - - ---- Randomize a value by a certain amount. --- @param #RAT self --- @param #number value The value which should be randomized --- @param #number fac Randomization factor. --- @param #number lower (Optional) Lower limit of the returned value. --- @param #number upper (Optional) Upper limit of the returned value. --- @return #number Randomized value. --- @usage _Randomize(100, 0.1) returns a value between 90 and 110, i.e. a plus/minus ten percent variation. --- @usage _Randomize(100, 0.5, nil, 120) returns a value between 50 and 120, i.e. a plus/minus fivty percent variation with upper bound 120. -function RAT:_Randomize(value, fac, lower, upper) - local min - if lower then - min=math.max(value-value*fac, lower) - else - min=value-value*fac - end - local max - if upper then - max=math.min(value+value*fac, upper) - else - max=value+value*fac - end - - local r=math.random(min, max) - - -- debug info - if self.debug then - local text=string.format("Random: value = %6.2f, fac = %4.2f, min = %6.2f, max = %6.2f, r = %6.2f", value, fac, min, max, r) - env.info(RAT.id..text) - end - - return r -end - ---- Generate Gaussian pseudo-random numbers. --- @param #number x0 Expectation value of distribution. --- @param #number sigma (Optional) Standard deviation. Default 10. --- @param #number xmin (Optional) Lower cut-off value. --- @param #number xmax (Optional) Upper cut-off value. --- @return #number Gaussian random number. -function RAT:_Random_Gaussian(x0, sigma, xmin, xmax) - - -- Standard deviation. Default 10 if not given. - sigma=sigma or 10 - - local r - local gotit=false - local i=0 - while not gotit do - - -- Uniform numbers in [0,1). We need two. - local x1=math.random() - local x2=math.random() - - -- Transform to Gaussian exp(-(x-x0)²/(2*sigma²). - r = math.sqrt(-2*sigma*sigma * math.log(x1)) * math.cos(2*math.pi * x2) + x0 - - i=i+1 - if (r>=xmin and r<=xmax) or i>100 then - gotit=true - end - end - - return r - ---old version ---[[ - -- Standard deviation. Default 10 if not given. - sigma=sigma or 10 - - -- Uniform numbers in [0,1). We need two. - local x1=math.random() - local x2=math.random() - - -- Transform to Gaussian exp(-(x-x0)²/(2*sigma²). - local r = math.sqrt(-2*sigma*sigma * math.log(x1)) * math.cos(2*math.pi * x2)+x0 - --local r2 = math.sqrt(-2*sigma*sigma * math.log(x1)) * math.sin(2*math.pi * x2)+x0 - - -- Cut-off distribution at xmin. - if xmin then - if rxmax then - if xmin then - r=math.max(math.min(x0,xmax),xmin) - else - r=math.min(x0,xmax) - end - end - end - - return r -]] -end - ---- Place markers of the waypoints. Note we assume a very specific number and type of waypoints here. --- @param #RAT self --- @param #table waypoints Table with waypoints. -function RAT:_PlaceMarkers(waypoints) - self:_SetMarker("Takeoff", waypoints[1]) - self:_SetMarker("Climb", waypoints[2]) - self:_SetMarker("Begin of Cruise", waypoints[3]) - self:_SetMarker("End of Cruise", waypoints[4]) - self:_SetMarker("Descent", waypoints[5]) - self:_SetMarker("Holding Point", waypoints[6]) - self:_SetMarker("Destination", waypoints[7]) -end - - ---- Set a marker visible for all on the F10 map. --- @param #RAT self --- @param #string text Info text displayed at maker. --- @param #table wp Position of marker coming in as waypoint, i.e. has x, y and alt components. -function RAT:_SetMarker(text, wp) - RAT.markerid=RAT.markerid+1 - self.markerids[#self.markerids+1]=RAT.markerid - if self.debug then - env.info(RAT.id..self.SpawnTemplatePrefix..": placing marker with ID "..RAT.markerid..": "..text) - end - -- Convert to coordinate. - local vec={x=wp.x, y=wp.alt, z=wp.y} - -- Place maker visible for all on the F10 map. - local text1=string.format("%s:\n%s", self.alias, text) - trigger.action.markToAll(RAT.markerid, text1, vec) -end - ---- Delete all markers on F10 map. --- @param #RAT self -function RAT:_DeleteMarkers() - for k,v in ipairs(self.markerids) do - trigger.action.removeMark(v) - end - for k,v in ipairs(self.markerids) do - self.markerids[k]=nil - end -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Modifies the template of the group to be spawned. --- In particular, the waypoints of the group's flight plan are copied into the spawn template. --- This allows to spawn at airports and also land at other airports, i.e. circumventing the DCS "landing bug". --- @param #RAT self --- @param #table waypoints The waypoints of the AI flight plan. -function RAT:_ModifySpawnTemplate(waypoints) - - -- The 3D vector of the first waypoint, i.e. where we actually spawn the template group. - local PointVec3 = {x=waypoints[1].x, y=waypoints[1].alt, z=waypoints[1].y} - - -- Heading from first to seconds waypoints to align units in case of air start. - local heading = self:_Course(waypoints[1], waypoints[2]) - - if self:_GetSpawnIndex(self.SpawnIndex+1) then - - local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate - - if SpawnTemplate then - self:T(SpawnTemplate) - - -- Translate the position of the Group Template to the Vec3. - for UnitID = 1, #SpawnTemplate.units do - self:T('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) - - -- Tranlate position. - local UnitTemplate = SpawnTemplate.units[UnitID] - local SX = UnitTemplate.x - local SY = UnitTemplate.y - local BX = SpawnTemplate.route.points[1].x - local BY = SpawnTemplate.route.points[1].y - local TX = PointVec3.x + (SX-BX) - local TY = PointVec3.z + (SY-BY) - SpawnTemplate.units[UnitID].x = TX - SpawnTemplate.units[UnitID].y = TY - SpawnTemplate.units[UnitID].alt = PointVec3.y - SpawnTemplate.units[UnitID].heading = math.rad(heading) - - -- Set (another) livery. - if self.livery then - SpawnTemplate.units[UnitID].livery_id = self.livery[math.random(#self.livery)] - end - - --SpawnTemplate.units[UnitID]["type"] = "Tu-142" - - -- Set AI skill. - SpawnTemplate.units[UnitID]["skill"] = self.skill - - -- Onboard number. - SpawnTemplate.units[UnitID]["onboard_num"] = self.SpawnIndex - - -- Modify coaltion and country of template. - SpawnTemplate.CoalitionID=self.coalition - if self.country then - SpawnTemplate.CountryID=self.country - end - - -- Parking spot. - UnitTemplate.parking = nil - UnitTemplate.parking_id = nil - - -- Initial altitude - UnitTemplate.alt=PointVec3.y - - self:T('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) - - end - - -- Copy waypoints into spawntemplate. By this we avoid the nasty DCS "landing bug" :) - for i,wp in ipairs(waypoints) do - SpawnTemplate.route.points[i]=wp - end - - -- Also modify x,y of the template. Not sure why. - SpawnTemplate.x = PointVec3.x - SpawnTemplate.y = PointVec3.z - --SpawnTemplate.uncontrolled=true - - -- Update modified template for spawn group. - self.SpawnGroups[self.SpawnIndex].SpawnTemplate=SpawnTemplate - - self:T(SpawnTemplate) - end - end -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Initializes the ATC arrays and starts schedulers. --- @param #RAT self --- @param #table airports_map List of all airports of the map. -function RAT:_ATCInit(airports_map) - if not RAT.ATC.init then - env.info(RAT.id.."Starting RAT ATC.") - RAT.ATC.init=true - for _,ap in pairs(airports_map) do - local name=ap:GetName() - RAT.ATC.airport[name]={} - RAT.ATC.airport[name].queue={} - RAT.ATC.airport[name].busy=false - RAT.ATC.airport[name].onfinal=nil - RAT.ATC.airport[name].traffic=0 - end - SCHEDULER:New(nil, RAT._ATCCheck, {self}, 5, 15) - SCHEDULER:New(nil, RAT._ATCStatus, {self}, 5, 60) - RAT.ATC.T0=timer.getTime() - end -end - ---- Adds andd initializes a new flight after it was spawned. --- @param #RAT self --- @param #string name Group name of the flight. --- @param #string dest Name of the destination airport. -function RAT:_ATCAddFlight(name, dest) - env.info(string.format("%s%s ATC: Adding flight %s with destination %s.", RAT.id, dest, name, dest)) - RAT.ATC.flight[name]={} - RAT.ATC.flight[name].destination=dest - RAT.ATC.flight[name].Tarrive=-1 - RAT.ATC.flight[name].holding=-1 - RAT.ATC.flight[name].Tonfinal=-1 -end - ---- Deletes a flight from ATC lists after it landed. --- @param #RAT self --- @param #table t Table. --- @param #string entry Flight name which shall be deleted. -function RAT:_ATCDelFlight(t,entry) - for k,_ in pairs(t) do - if k==entry then - t[entry]=nil - end - end -end - ---- Registers a flight once it is near its holding point at the final destination. --- @param #RAT self --- @param #string name Group name of the flight. --- @param #number time Time the fight first registered. -function RAT:_ATCRegisterFlight(name, time) - RAT.ATC.flight[name].Tarrive=time - RAT.ATC.flight[name].holding=0 -end - - ---- ATC status report about flights. --- @param #RAT self -function RAT:_ATCStatus() - - -- Current time. - local Tnow=timer.getTime() - - for name,_ in pairs(RAT.ATC.flight) do - - local hold=RAT.ATC.flight[name].holding - local dest=RAT.ATC.flight[name].destination - - if hold >= 0 then - - -- Some string whether the runway is busy or not. - local busy="Runway is currently clear" - if RAT.ATC.airport[dest].busy then - if RAT.ATC.airport[dest].onfinal then - busy="Runway is occupied by "..RAT.ATC.airport[dest].onfinal - else - busy="Runway is occupied" - end - end - - -- Aircraft is holding. - env.info(string.format("%s%s ATC: Flight %s is holding for %i:%02d. %s.", RAT.id, dest, name, hold/60, hold%60, busy)) - - elseif hold==RAT.ATC.onfinal then - - -- Aircarft is on final approach for landing. - local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal - env.info(string.format("%s%s ATC: Flight %s was cleared for landing. Waiting %i:%02d for landing event.", RAT.id, dest, name, Tfinal/60, Tfinal%60)) - - --TODO: Trigger landing for another aircraft when Tfinal > x min? - -- After five minutes we set the runway to green. ==> Increase the landing frequency a bit. - if Tfinal>300 then - --RAT.ATC.airport[dest].busy=false - end - - elseif hold==RAT.ATC.unregistered then - - -- Aircraft has not arrived at holding point. - --env.info(string.format("%s ATC: Flight %s is not registered yet (hold %d).", dest, name, hold)) - - else - env.error(RAT.id.."Unknown holding time in RAT:_ATCStatus().") - end - end - -end - ---- Main ATC function. Updates the landing queue of all airports and inceases holding time for all flights. --- @param #RAT self -function RAT:_ATCCheck() - - -- Init queue of flights at all airports. - RAT:_ATCQueue() - - -- Current time. - local Tnow=timer.getTime() - - for name,_ in pairs(RAT.ATC.airport) do - - - -- List of flights cleared for landing. - local qw={} - - for qID,flight in ipairs(RAT.ATC.airport[name].queue) do - - -- Number of aircraft in queue. - local nqueue=#RAT.ATC.airport[name].queue - - if RAT.ATC.airport[name].busy then - - -- Update holding time. - RAT.ATC.flight[flight].holding=Tnow-RAT.ATC.flight[flight].Tarrive - - -- Debug message. - local text=string.format("%s ATC: Flight %s runway is busy. You are #%d of %d in landing queue. Your holding time is %i:%02d.", name, flight,qID, nqueue, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) - env.info(text) - - else - - -- Clear flight for landing. - RAT:_ATCClearForLanding(name, flight) - table.insert(qw, qID) - - end - - end - - -- Remove cleared flights from queue. - for _,i in pairs(qw) do - table.remove(RAT.ATC.airport[name].queue, i) - end - - end -end - ---- Giving landing clearance for aircraft by setting user flag. --- @param #RAT self --- @param #string airport Name of destination airport. --- @param #string flight Group name of flight, which gets landing clearence. -function RAT:_ATCClearForLanding(airport, flight) - -- Flight is cleared for landing. - RAT.ATC.flight[flight].holding=RAT.ATC.onfinal - -- Airport runway is busy now. - RAT.ATC.airport[airport].busy=true - -- Flight which is landing. - RAT.ATC.airport[airport].onfinal=flight - -- Current time. - RAT.ATC.flight[flight].Tonfinal=timer.getTime() - -- Set user flag to 1 ==> stop condition for holding. - trigger.action.setUserFlag(flight, 1) - local flagvalue=trigger.misc.getUserFlag(flight) - - -- Debug message. - local text1=string.format("%s%s ATC: Flight %s cleared for final approach (flag=%d).", RAT.id, airport, flight, flagvalue) - local text2=string.format("%s ATC: Flight %s you are cleared for landing.", airport, flight) - env.info(text1) - MESSAGE:New(text2, 10):ToAll() -end - ---- Takes care of organisational stuff after a plane has landed. --- @param #RAT self --- @param #string name Group name of flight. -function RAT:_ATCFlightLanded(name) - - if RAT.ATC.flight[name] then - - -- Destination airport. - local dest=RAT.ATC.flight[name].destination - - -- Times for holding and final approach. - local Tnow=timer.getTime() - local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal - local Thold=RAT.ATC.flight[name].Tonfinal-RAT.ATC.flight[name].Tarrive - - -- Airport is not busy any more. - RAT.ATC.airport[dest].busy=false - - -- No aircraft on final any more. - RAT.ATC.airport[dest].onfinal=nil - - -- Remove this flight from list of flights. - RAT:_ATCDelFlight(RAT.ATC.flight, name) - - -- Increase landing counter to monitor traffic. - RAT.ATC.airport[dest].traffic=RAT.ATC.airport[dest].traffic+1 - - -- Debug info - local text1=string.format("%s%s ATC: Flight %s landed. Tholding = %i:%02d, Tfinal = %i:%02d.", RAT.id, dest, name, Thold/60, Thold%60, Tfinal/60, Tfinal%60) - local text2=string.format("%s ATC: Flight %s landed. Welcome to %s.", dest, name, dest) - env.info(text1) - env.info(string.format("%s%s ATC: Number of planes landed in total %d.", RAT.id, dest, RAT.ATC.airport[dest].traffic)) - MESSAGE:New(text2, 10):ToAll() - end - -end - ---- Creates a landing queue for all flights holding at airports. Aircraft with longest holding time gets first permission to land. --- @param #RAT self -function RAT:_ATCQueue() - - for airport,_ in pairs(RAT.ATC.airport) do - - -- Local airport queue. - local _queue={} - - -- Loop over all flights. - for name,_ in pairs(RAT.ATC.flight) do - - local hold=RAT.ATC.flight[name].holding - local dest=RAT.ATC.flight[name].destination - - -- Flight is holding at this airport. - if hold>=0 and airport==dest then - _queue[#_queue+1]={name,hold} - end - end - - -- Sort queue w.r.t holding time in acending order. - local function compare(a,b) - return a[2] > b[2] - end - table.sort(_queue, compare) - - -- Transfer queue to airport queue. - RAT.ATC.airport[airport].queue={} - for k,v in ipairs(_queue) do - table.insert(RAT.ATC.airport[airport].queue, v[1]) - end - - end -end - ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- **AI** -- **AI Balancing will replace in multi player missions --- non-occupied human slots with AI groups, in order to provide an engaging simulation environment, --- even when there are hardly any players in the mission.** --- --- ![Banner Image](..\Presentations\AI_Balancer\Dia1.JPG) --- --- ==== --- --- # Demo Missions --- --- ### [AI_BALANCER Demo Missions source code](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/AIB%20-%20AI%20Balancing) --- --- ### [AI_BALANCER Demo Missions, only for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AIB%20-%20AI%20Balancing) --- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) --- --- ==== --- --- # YouTube Channel --- --- ### [AI_BALANCER YouTube Channel](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl2CJVIrL1TdAumuVS8n64B7) --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- ### Contributions: --- --- * **[Dutch_Baron](https://forums.eagle.ru/member.php?u=112075)**: Working together with James has resulted in the creation of the AI_BALANCER class. James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-) --- --- ==== --- --- @module AI_Balancer - ---- @type AI_BALANCER --- @field Core.Set#SET_CLIENT SetClient --- @field Functional.Spawn#SPAWN SpawnAI --- @field Wrapper.Group#GROUP Test --- @extends Core.Fsm#FSM_SET - - ---- # AI_BALANCER class, extends @{Fsm#FSM_SET} --- --- The AI_BALANCER class monitors and manages as many replacement AI groups as there are --- CLIENTS in a SET_CLIENT collection, which are not occupied by human players. --- In other words, use AI_BALANCER to simulate human behaviour by spawning in replacement AI in multi player missions. --- --- The parent class @{Fsm#FSM_SET} manages the functionality to control the Finite State Machine (FSM). --- The mission designer can tailor the behaviour of the AI_BALANCER, by defining event and state transition methods. --- An explanation about state and event transition methods can be found in the @{FSM} module documentation. --- --- The mission designer can tailor the AI_BALANCER behaviour, by implementing a state or event handling method for the following: --- --- * @{#AI_BALANCER.OnAfterSpawned}( AISet, From, Event, To, AIGroup ): Define to add extra logic when an AI is spawned. --- --- ## 1. AI_BALANCER construction --- --- Create a new AI_BALANCER object with the @{#AI_BALANCER.New}() method: --- --- ## 2. AI_BALANCER is a FSM --- --- ![Process](..\Presentations\AI_Balancer\Dia13.JPG) --- --- ### 2.1. AI_BALANCER States --- --- * **Monitoring** ( Set ): Monitoring the Set if all AI is spawned for the Clients. --- * **Spawning** ( Set, ClientName ): There is a new AI group spawned with ClientName as the name of reference. --- * **Spawned** ( Set, AIGroup ): A new AI has been spawned. You can handle this event to customize the AI behaviour with other AI FSMs or own processes. --- * **Destroying** ( Set, AIGroup ): The AI is being destroyed. --- * **Returning** ( Set, AIGroup ): The AI is returning to the airbase specified by the ReturnToAirbase methods. Handle this state to customize the return behaviour of the AI, if any. --- --- ### 2.2. AI_BALANCER Events --- --- * **Monitor** ( Set ): Every 10 seconds, the Monitor event is triggered to monitor the Set. --- * **Spawn** ( Set, ClientName ): Triggers when there is a new AI group to be spawned with ClientName as the name of reference. --- * **Spawned** ( Set, AIGroup ): Triggers when a new AI has been spawned. You can handle this event to customize the AI behaviour with other AI FSMs or own processes. --- * **Destroy** ( Set, AIGroup ): The AI is being destroyed. --- * **Return** ( Set, AIGroup ): The AI is returning to the airbase specified by the ReturnToAirbase methods. --- --- ## 3. AI_BALANCER spawn interval for replacement AI --- --- Use the method @{#AI_BALANCER.InitSpawnInterval}() to set the earliest and latest interval in seconds that is waited until a new replacement AI is spawned. --- --- ## 4. AI_BALANCER returns AI to Airbases --- --- By default, When a human player joins a slot that is AI_BALANCED, the AI group will be destroyed by default. --- However, there are 2 additional options that you can use to customize the destroy behaviour. --- When a human player joins a slot, you can configure to let the AI return to: --- --- * @{#AI_BALANCER.ReturnToHomeAirbase}: Returns the AI to the **home** @{Airbase#AIRBASE}. --- * @{#AI_BALANCER.ReturnToNearestAirbases}: Returns the AI to the **nearest friendly** @{Airbase#AIRBASE}. --- --- Note that when AI returns to an airbase, the AI_BALANCER will trigger the **Return** event and the AI will return, --- otherwise the AI_BALANCER will trigger a **Destroy** event, and the AI will be destroyed. --- --- @field #AI_BALANCER -AI_BALANCER = { - ClassName = "AI_BALANCER", - PatrolZones = {}, - AIGroups = {}, - Earliest = 5, -- Earliest a new AI can be spawned is in 5 seconds. - Latest = 60, -- Latest a new AI can be spawned is in 60 seconds. -} - - - ---- Creates a new AI_BALANCER object --- @param #AI_BALANCER self --- @param Core.Set#SET_CLIENT SetClient A SET\_CLIENT object that will contain the CLIENT objects to be monitored if they are alive or not (joined by a player). --- @param Functional.Spawn#SPAWN SpawnAI The default Spawn object to spawn new AI Groups when needed. --- @return #AI_BALANCER -function AI_BALANCER:New( SetClient, SpawnAI ) - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_SET:New( SET_GROUP:New() ) ) -- AI.AI_Balancer#AI_BALANCER - - -- TODO: Define the OnAfterSpawned event - self:SetStartState( "None" ) - self:AddTransition( "*", "Monitor", "Monitoring" ) - self:AddTransition( "*", "Spawn", "Spawning" ) - self:AddTransition( "Spawning", "Spawned", "Spawned" ) - self:AddTransition( "*", "Destroy", "Destroying" ) - self:AddTransition( "*", "Return", "Returning" ) - - self.SetClient = SetClient - self.SetClient:FilterOnce() - self.SpawnAI = SpawnAI - - self.SpawnQueue = {} - - self.ToNearestAirbase = false - self.ToHomeAirbase = false - - self:__Monitor( 1 ) - - return self -end - ---- Sets the earliest to the latest interval in seconds how long AI_BALANCER will wait to spawn a new AI. --- Provide 2 identical seconds if the interval should be a fixed amount of seconds. --- @param #AI_BALANCER self --- @param #number Earliest The earliest a new AI can be spawned in seconds. --- @param #number Latest The latest a new AI can be spawned in seconds. --- @return self -function AI_BALANCER:InitSpawnInterval( Earliest, Latest ) - - self.Earliest = Earliest - self.Latest = Latest - - return self -end - ---- Returns the AI to the nearest friendly @{Airbase#AIRBASE}. --- @param #AI_BALANCER self --- @param Dcs.DCSTypes#Distance ReturnThresholdRange If there is an enemy @{Client#CLIENT} within the ReturnThresholdRange given in meters, the AI will not return to the nearest @{Airbase#AIRBASE}. --- @param Core.Set#SET_AIRBASE ReturnAirbaseSet The SET of @{Set#SET_AIRBASE}s to evaluate where to return to. -function AI_BALANCER:ReturnToNearestAirbases( ReturnThresholdRange, ReturnAirbaseSet ) - - self.ToNearestAirbase = true - self.ReturnThresholdRange = ReturnThresholdRange - self.ReturnAirbaseSet = ReturnAirbaseSet -end - ---- Returns the AI to the home @{Airbase#AIRBASE}. --- @param #AI_BALANCER self --- @param Dcs.DCSTypes#Distance ReturnThresholdRange If there is an enemy @{Client#CLIENT} within the ReturnThresholdRange given in meters, the AI will not return to the nearest @{Airbase#AIRBASE}. -function AI_BALANCER:ReturnToHomeAirbase( ReturnThresholdRange ) - - self.ToHomeAirbase = true - self.ReturnThresholdRange = ReturnThresholdRange -end - ---- @param #AI_BALANCER self --- @param Core.Set#SET_GROUP SetGroup --- @param #string ClientName --- @param Wrapper.Group#GROUP AIGroup -function AI_BALANCER:onenterSpawning( SetGroup, From, Event, To, ClientName ) - - -- OK, Spawn a new group from the default SpawnAI object provided. - local AIGroup = self.SpawnAI:Spawn() -- Wrapper.Group#GROUP - if AIGroup then - AIGroup:E( "Spawning new AIGroup" ) - --TODO: need to rework UnitName thing ... - - SetGroup:Add( ClientName, AIGroup ) - self.SpawnQueue[ClientName] = nil - - -- Fire the Spawned event. The first parameter is the AIGroup just Spawned. - -- Mission designers can catch this event to bind further actions to the AIGroup. - self:Spawned( AIGroup ) - end -end - ---- @param #AI_BALANCER self --- @param Core.Set#SET_GROUP SetGroup --- @param Wrapper.Group#GROUP AIGroup -function AI_BALANCER:onenterDestroying( SetGroup, From, Event, To, ClientName, AIGroup ) - - AIGroup:Destroy() - SetGroup:Flush() - SetGroup:Remove( ClientName ) - SetGroup:Flush() -end - ---- @param #AI_BALANCER self --- @param Core.Set#SET_GROUP SetGroup --- @param Wrapper.Group#GROUP AIGroup -function AI_BALANCER:onenterReturning( SetGroup, From, Event, To, AIGroup ) - - local AIGroupTemplate = AIGroup:GetTemplate() - if self.ToHomeAirbase == true then - local WayPointCount = #AIGroupTemplate.route.points - local SwitchWayPointCommand = AIGroup:CommandSwitchWayPoint( 1, WayPointCount, 1 ) - AIGroup:SetCommand( SwitchWayPointCommand ) - AIGroup:MessageToRed( "Returning to home base ...", 30 ) - else - -- Okay, we need to send this Group back to the nearest base of the Coalition of the AI. - --TODO: i need to rework the POINT_VEC2 thing. - local PointVec2 = POINT_VEC2:New( AIGroup:GetVec2().x, AIGroup:GetVec2().y ) - local ClosestAirbase = self.ReturnAirbaseSet:FindNearestAirbaseFromPointVec2( PointVec2 ) - self:T( ClosestAirbase.AirbaseName ) - AIGroup:MessageToRed( "Returning to " .. ClosestAirbase:GetName().. " ...", 30 ) - local RTBRoute = AIGroup:RouteReturnToAirbase( ClosestAirbase ) - AIGroupTemplate.route = RTBRoute - AIGroup:Respawn( AIGroupTemplate ) - end - -end - - ---- @param #AI_BALANCER self -function AI_BALANCER:onenterMonitoring( SetGroup ) - - self:T2( { self.SetClient:Count() } ) - --self.SetClient:Flush() - - self.SetClient:ForEachClient( - --- @param Wrapper.Client#CLIENT Client - function( Client ) - self:T3(Client.ClientName) - - local AIGroup = self.Set:Get( Client.UnitName ) -- Wrapper.Group#GROUP - if Client:IsAlive() then - - if AIGroup and AIGroup:IsAlive() == true then - - if self.ToNearestAirbase == false and self.ToHomeAirbase == false then - self:Destroy( Client.UnitName, AIGroup ) - else - -- We test if there is no other CLIENT within the self.ReturnThresholdRange of the first unit of the AI group. - -- If there is a CLIENT, the AI stays engaged and will not return. - -- If there is no CLIENT within the self.ReturnThresholdRange, then the unit will return to the Airbase return method selected. - - local PlayerInRange = { Value = false } - local RangeZone = ZONE_RADIUS:New( 'RangeZone', AIGroup:GetVec2(), self.ReturnThresholdRange ) - - self:T2( RangeZone ) - - _DATABASE:ForEachPlayer( - --- @param Wrapper.Unit#UNIT RangeTestUnit - function( RangeTestUnit, RangeZone, AIGroup, PlayerInRange ) - self:T2( { PlayerInRange, RangeTestUnit.UnitName, RangeZone.ZoneName } ) - if RangeTestUnit:IsInZone( RangeZone ) == true then - self:T2( "in zone" ) - if RangeTestUnit:GetCoalition() ~= AIGroup:GetCoalition() then - self:T2( "in range" ) - PlayerInRange.Value = true - end - end - end, - - --- @param Core.Zone#ZONE_RADIUS RangeZone - -- @param Wrapper.Group#GROUP AIGroup - function( RangeZone, AIGroup, PlayerInRange ) - if PlayerInRange.Value == false then - self:Return( AIGroup ) - end - end - , RangeZone, AIGroup, PlayerInRange - ) - - end - self.Set:Remove( Client.UnitName ) - end - else - if not AIGroup or not AIGroup:IsAlive() == true then - self:T( "Client " .. Client.UnitName .. " not alive." ) - if not self.SpawnQueue[Client.UnitName] then - -- Spawn a new AI taking into account the spawn interval Earliest, Latest - self:__Spawn( math.random( self.Earliest, self.Latest ), Client.UnitName ) - self.SpawnQueue[Client.UnitName] = true - self:E( "New AI Spawned for Client " .. Client.UnitName ) - end - end - end - return true - end - ) - - self:__Monitor( 10 ) -end - - - ---- **AI** -- **AI A2A Air Patrolling or Staging.** --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- ### Contributions: --- --- * **[Dutch_Baron](https://forums.eagle.ru/member.php?u=112075)**: Working together with James has resulted in the creation of the AI_BALANCER class. James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-) --- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Testing and API concept review. --- --- ==== --- --- @module AI_A2A - ---BASE:TraceClass("AI_A2A") - - ---- @type AI_A2A --- @extends Core.Fsm#FSM_CONTROLLABLE - ---- # AI_A2A class, extends @{Fsm#FSM_CONTROLLABLE} --- --- The AI_A2A class implements the core functions to operate an AI @{Group} A2A tasking. --- --- --- ## AI_A2A constructor --- --- * @{#AI_A2A.New}(): Creates a new AI_A2A object. --- --- ## 2. AI_A2A is a FSM --- --- ![Process](..\Presentations\AI_PATROL\Dia2.JPG) --- --- ### 2.1. AI_A2A States --- --- * **None** ( Group ): The process is not started yet. --- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. --- * **Returning** ( Group ): The AI is returning to Base. --- * **Stopped** ( Group ): The process is stopped. --- * **Crashed** ( Group ): The AI has crashed or is dead. --- --- ### 2.2. AI_A2A Events --- --- * **Start** ( Group ): Start the process. --- * **Stop** ( Group ): Stop the process. --- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. --- * **RTB** ( Group ): Route the AI to the home base. --- * **Detect** ( Group ): The AI is detecting targets. --- * **Detected** ( Group ): The AI has detected new targets. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- ## 3. Set or Get the AI controllable --- --- * @{#AI_A2A.SetControllable}(): Set the AIControllable. --- * @{#AI_A2A.GetControllable}(): Get the AIControllable. --- --- @field #AI_A2A -AI_A2A = { - ClassName = "AI_A2A", -} - ---- Creates a new AI_A2A object --- @param #AI_A2A self --- @param Wrapper.Group#GROUP AIGroup The GROUP object to receive the A2A Process. --- @return #AI_A2A -function AI_A2A:New( AIGroup ) - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_A2A - - self:SetControllable( AIGroup ) - - self:SetFuelThreshold( .2, 60 ) - self:SetDamageThreshold( 0.4 ) - self:SetDisengageRadius( 70000 ) - - self:SetStartState( "Stopped" ) - - self:AddTransition( "*", "Start", "Started" ) - - --- Start Handler OnBefore for AI_A2A - -- @function [parent=#AI_A2A] OnBeforeStart - -- @param #AI_A2A self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- Start Handler OnAfter for AI_A2A - -- @function [parent=#AI_A2A] OnAfterStart - -- @param #AI_A2A self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- Start Trigger for AI_A2A - -- @function [parent=#AI_A2A] Start - -- @param #AI_A2A self - - --- Start Asynchronous Trigger for AI_A2A - -- @function [parent=#AI_A2A] __Start - -- @param #AI_A2A self - -- @param #number Delay - - self:AddTransition( "*", "Stop", "Stopped" ) - ---- OnLeave Transition Handler for State Stopped. --- @function [parent=#AI_A2A] OnLeaveStopped --- @param #AI_A2A self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Stopped. --- @function [parent=#AI_A2A] OnEnterStopped --- @param #AI_A2A self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- OnBefore Transition Handler for Event Stop. --- @function [parent=#AI_A2A] OnBeforeStop --- @param #AI_A2A self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Stop. --- @function [parent=#AI_A2A] OnAfterStop --- @param #AI_A2A self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Stop. --- @function [parent=#AI_A2A] Stop --- @param #AI_A2A self - ---- Asynchronous Event Trigger for Event Stop. --- @function [parent=#AI_A2A] __Stop --- @param #AI_A2A self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A. - ---- OnBefore Transition Handler for Event Status. --- @function [parent=#AI_A2A] OnBeforeStatus --- @param #AI_A2A self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Status. --- @function [parent=#AI_A2A] OnAfterStatus --- @param #AI_A2A self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Status. --- @function [parent=#AI_A2A] Status --- @param #AI_A2A self - ---- Asynchronous Event Trigger for Event Status. --- @function [parent=#AI_A2A] __Status --- @param #AI_A2A self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "RTB", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A. - ---- OnBefore Transition Handler for Event RTB. --- @function [parent=#AI_A2A] OnBeforeRTB --- @param #AI_A2A self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event RTB. --- @function [parent=#AI_A2A] OnAfterRTB --- @param #AI_A2A self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event RTB. --- @function [parent=#AI_A2A] RTB --- @param #AI_A2A self - ---- Asynchronous Event Trigger for Event RTB. --- @function [parent=#AI_A2A] __RTB --- @param #AI_A2A self --- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Returning. --- @function [parent=#AI_A2A] OnLeaveReturning --- @param #AI_A2A self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Returning. --- @function [parent=#AI_A2A] OnEnterReturning --- @param #AI_A2A self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Patrolling", "Refuel", "Refuelling" ) - - --- Refuel Handler OnBefore for AI_A2A - -- @function [parent=#AI_A2A] OnBeforeRefuel - -- @param #AI_A2A self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- Refuel Handler OnAfter for AI_A2A - -- @function [parent=#AI_A2A] OnAfterRefuel - -- @param #AI_A2A self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From - -- @param #string Event - -- @param #string To - - --- Refuel Trigger for AI_A2A - -- @function [parent=#AI_A2A] Refuel - -- @param #AI_A2A self - - --- Refuel Asynchronous Trigger for AI_A2A - -- @function [parent=#AI_A2A] __Refuel - -- @param #AI_A2A self - -- @param #number Delay - - self:AddTransition( "*", "Takeoff", "Airborne" ) - self:AddTransition( "*", "Return", "Returning" ) - self:AddTransition( "*", "Hold", "Holding" ) - self:AddTransition( "*", "Home", "Home" ) - self:AddTransition( "*", "LostControl", "LostControl" ) - self:AddTransition( "*", "Fuel", "Fuel" ) - self:AddTransition( "*", "Damaged", "Damaged" ) - self:AddTransition( "*", "Eject", "*" ) - self:AddTransition( "*", "Crash", "Crashed" ) - self:AddTransition( "*", "PilotDead", "*" ) - - self.IdleCount = 0 - - return self -end - ---- @param Wrapper.Group#GROUP self --- @param Core.Event#EVENTDATA EventData -function GROUP:OnEventTakeoff( EventData, Fsm ) - Fsm:Takeoff() - self:UnHandleEvent( EVENTS.Takeoff ) -end - -function AI_A2A:SetDispatcher( Dispatcher ) - self.Dispatcher = Dispatcher -end - -function AI_A2A:GetDispatcher() - return self.Dispatcher -end - -function AI_A2A:SetTargetDistance( Coordinate ) - - local CurrentCoord = self.Controllable:GetCoordinate() - self.TargetDistance = CurrentCoord:Get2DDistance( Coordinate ) - - self.ClosestTargetDistance = ( not self.ClosestTargetDistance or self.ClosestTargetDistance > self.TargetDistance ) and self.TargetDistance or self.ClosestTargetDistance -end - - -function AI_A2A:ClearTargetDistance() - - self.TargetDistance = nil - self.ClosestTargetDistance = nil -end - - ---- Sets (modifies) the minimum and maximum speed of the patrol. --- @param #AI_A2A self --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @return #AI_A2A self -function AI_A2A:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) - self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) - - self.PatrolMinSpeed = PatrolMinSpeed - self.PatrolMaxSpeed = PatrolMaxSpeed -end - - ---- Sets the floor and ceiling altitude of the patrol. --- @param #AI_A2A self --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @return #AI_A2A self -function AI_A2A:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) - self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) - - self.PatrolFloorAltitude = PatrolFloorAltitude - self.PatrolCeilingAltitude = PatrolCeilingAltitude -end - - ---- Sets the home airbase. --- @param #AI_A2A self --- @param Wrapper.Airbase#AIRBASE HomeAirbase --- @return #AI_A2A self -function AI_A2A:SetHomeAirbase( HomeAirbase ) - self:F2( { HomeAirbase } ) - - self.HomeAirbase = HomeAirbase -end - ---- Sets to refuel at the given tanker. --- @param #AI_A2A self --- @param Wrapper.Group#GROUP TankerName The group name of the tanker as defined within the Mission Editor or spawned. --- @return #AI_A2A self -function AI_A2A:SetTanker( TankerName ) - self:F2( { TankerName } ) - - self.TankerName = TankerName -end - - ---- Sets the disengage range, that when engaging a target beyond the specified range, the engagement will be cancelled and the plane will RTB. --- @param #AI_A2A self --- @param #number DisengageRadius The disengage range. --- @return #AI_A2A self -function AI_A2A:SetDisengageRadius( DisengageRadius ) - self:F2( { DisengageRadius } ) - - self.DisengageRadius = DisengageRadius -end - ---- Set the status checking off. --- @param #AI_A2A self --- @return #AI_A2A self -function AI_A2A:SetStatusOff() - self:F2() - - self.CheckStatus = false -end - - ---- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. --- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. --- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted to the AI_A2A. --- Once the time is finished, the old AI will return to the base. --- @param #AI_A2A self --- @param #number PatrolFuelThresholdPercentage The treshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. --- @param #number PatrolOutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base. --- @return #AI_A2A self -function AI_A2A:SetFuelThreshold( PatrolFuelThresholdPercentage, PatrolOutOfFuelOrbitTime ) - - self.PatrolManageFuel = true - self.PatrolFuelThresholdPercentage = PatrolFuelThresholdPercentage - self.PatrolOutOfFuelOrbitTime = PatrolOutOfFuelOrbitTime - - self.Controllable:OptionRTBBingoFuel( false ) - - return self -end - ---- When the AI is damaged beyond a certain treshold, it is required that the AI returns to the home base. --- However, damage cannot be foreseen early on. --- Therefore, when the damage treshold is reached, --- the AI will return immediately to the home base (RTB). --- Note that for groups, the average damage of the complete group will be calculated. --- So, in a group of 4 airplanes, 2 lost and 2 with damage 0.2, the damage treshold will be 0.25. --- @param #AI_A2A self --- @param #number PatrolDamageThreshold The treshold in percentage (between 0 and 1) when the AI is considered to be damaged. --- @return #AI_A2A self -function AI_A2A:SetDamageThreshold( PatrolDamageThreshold ) - - self.PatrolManageDamage = true - self.PatrolDamageThreshold = PatrolDamageThreshold - - return self -end - ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. --- @param #AI_A2A self --- @return #AI_A2A self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A:onafterStart( Controllable, From, Event, To ) - self:F2() - - self:__Status( 10 ) -- Check status status every 30 seconds. - - self:HandleEvent( EVENTS.PilotDead, self.OnPilotDead ) - self:HandleEvent( EVENTS.Crash, self.OnCrash ) - self:HandleEvent( EVENTS.Ejection, self.OnEjection ) - - Controllable:OptionROEHoldFire() - Controllable:OptionROTVertical() -end - - - ---- @param #AI_A2A self -function AI_A2A:onbeforeStatus() - - return self.CheckStatus -end - ---- @param #AI_A2A self -function AI_A2A:onafterStatus() - - self:F( " Checking Status" ) - - if self.Controllable and self.Controllable:IsAlive() then - - local RTB = false - - local DistanceFromHomeBase = self.HomeAirbase:GetCoordinate():Get2DDistance( self.Controllable:GetCoordinate() ) - - if not self:Is( "Holding" ) and not self:Is( "Returning" ) then - local DistanceFromHomeBase = self.HomeAirbase:GetCoordinate():Get2DDistance( self.Controllable:GetCoordinate() ) - self:F({DistanceFromHomeBase=DistanceFromHomeBase}) - - if DistanceFromHomeBase > self.DisengageRadius then - self:E( self.Controllable:GetName() .. " is too far from home base, RTB!" ) - self:Hold( 300 ) - RTB = false - end - end - - if self:Is( "Fuel" ) or self:Is( "Damaged" ) or self:Is( "LostControl" ) then - if DistanceFromHomeBase < 5000 then - self:E( self.Controllable:GetName() .. " is too far from home base, RTB!" ) - self:Home( "Destroy" ) - end - end - - - if not self:Is( "Fuel" ) and not self:Is( "Home" ) then - local Fuel = self.Controllable:GetFuel() - self:F({Fuel=Fuel}) - if Fuel < self.PatrolFuelThresholdPercentage then - if self.TankerName then - self:E( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... Refuelling at Tanker!" ) - self:Refuel() - else - self:E( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... RTB!" ) - local OldAIControllable = self.Controllable - - local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) - local TimedOrbitTask = OldAIControllable:TaskControlled( OrbitTask, OldAIControllable:TaskCondition(nil,nil,nil,nil,self.PatrolOutOfFuelOrbitTime,nil ) ) - OldAIControllable:SetTask( TimedOrbitTask, 10 ) - - self:Fuel() - RTB = true - end - else - end - end - - -- TODO: Check GROUP damage function. - local Damage = self.Controllable:GetLife() - local InitialLife = self.Controllable:GetLife0() - self:F( { Damage = Damage, InitialLife = InitialLife, DamageThreshold = self.PatrolDamageThreshold } ) - if ( Damage / InitialLife ) < self.PatrolDamageThreshold then - self:E( self.Controllable:GetName() .. " is damaged: " .. Damage .. " ... RTB!" ) - self:Damaged() - RTB = true - self:SetStatusOff() - end - - -- Check if planes went RTB and are out of control. - if self.Controllable:HasTask() == false then - if not self:Is( "Started" ) and - not self:Is( "Stopped" ) and - not self:Is( "Home" ) then - if self.IdleCount >= 2 then - if Damage ~= InitialLife then - self:Damaged() - else - self:E( self.Controllable:GetName() .. " control lost! " ) - self:LostControl() - end - else - self.IdleCount = self.IdleCount + 1 - end - end - else - self.IdleCount = 0 - end - - if RTB == true then - self:__RTB( 0.5 ) - end - - self:__Status( 10 ) - end -end - - ---- @param Wrapper.Group#GROUP AIGroup -function AI_A2A.RTBRoute( AIGroup, Fsm ) - - AIGroup:F( { "AI_A2A.RTBRoute:", AIGroup:GetName() } ) - - if AIGroup:IsAlive() then - Fsm:__RTB( 0.5 ) - end - -end - ---- @param Wrapper.Group#GROUP AIGroup -function AI_A2A.RTBHold( AIGroup, Fsm ) - - AIGroup:F( { "AI_A2A.RTBHold:", AIGroup:GetName() } ) - if AIGroup:IsAlive() then - Fsm:__RTB( 0.5 ) - Fsm:Return() - local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) - AIGroup:SetTask( Task ) - end - -end - - ---- @param #AI_A2A self --- @param Wrapper.Group#GROUP AIGroup -function AI_A2A:onafterRTB( AIGroup, From, Event, To ) - self:F( { AIGroup, From, Event, To } ) - - - if AIGroup and AIGroup:IsAlive() then - - self:E( "Group " .. AIGroup:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" ) - - self:ClearTargetDistance() - AIGroup:ClearTasks() - - local EngageRoute = {} - - --- Calculate the target route point. - - local CurrentCoord = AIGroup:GetCoordinate() - local ToTargetCoord = self.HomeAirbase:GetCoordinate() - local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) - local ToAirbaseAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) ) - - local Distance = CurrentCoord:Get2DDistance( ToTargetCoord ) - - local ToAirbaseCoord = CurrentCoord:Translate( 5000, ToAirbaseAngle ) - if Distance < 5000 then - self:E( "RTB and near the airbase!" ) - self:Home() - return - end - --- Create a route point of type air. - local ToRTBRoutePoint = ToAirbaseCoord:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - self:F( { Angle = ToAirbaseAngle, ToTargetSpeed = ToTargetSpeed } ) - self:T2( { self.MinSpeed, self.MaxSpeed, ToTargetSpeed } ) - - EngageRoute[#EngageRoute+1] = ToRTBRoutePoint - EngageRoute[#EngageRoute+1] = ToRTBRoutePoint - - AIGroup:OptionROEHoldFire() - AIGroup:OptionROTEvadeFire() - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - AIGroup:WayPointInitialize( EngageRoute ) - - local Tasks = {} - Tasks[#Tasks+1] = AIGroup:TaskFunction( "AI_A2A.RTBRoute", self ) - EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( Tasks ) - - --- NOW ROUTE THE GROUP! - AIGroup:Route( EngageRoute, 0.5 ) - - end - -end - ---- @param #AI_A2A self --- @param Wrapper.Group#GROUP AIGroup -function AI_A2A:onafterHome( AIGroup, From, Event, To ) - self:F( { AIGroup, From, Event, To } ) - - self:E( "Group " .. self.Controllable:GetName() .. " ... Home! ( " .. self:GetState() .. " )" ) - - if AIGroup and AIGroup:IsAlive() then - end - -end - - - ---- @param #AI_A2A self --- @param Wrapper.Group#GROUP AIGroup -function AI_A2A:onafterHold( AIGroup, From, Event, To, HoldTime ) - self:F( { AIGroup, From, Event, To } ) - - self:E( "Group " .. self.Controllable:GetName() .. " ... Holding! ( " .. self:GetState() .. " )" ) - - if AIGroup and AIGroup:IsAlive() then - local OrbitTask = AIGroup:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) - local TimedOrbitTask = AIGroup:TaskControlled( OrbitTask, AIGroup:TaskCondition( nil, nil, nil, nil, HoldTime , nil ) ) - - local RTBTask = AIGroup:TaskFunction( "AI_A2A.RTBHold", self ) - - local OrbitHoldTask = AIGroup:TaskOrbitCircle( 4000, self.PatrolMinSpeed ) - - --AIGroup:SetState( AIGroup, "AI_A2A", self ) - - AIGroup:SetTask( AIGroup:TaskCombo( { TimedOrbitTask, RTBTask, OrbitHoldTask } ), 1 ) - end - -end - ---- @param Wrapper.Group#GROUP AIGroup -function AI_A2A.Resume( AIGroup, Fsm ) - - AIGroup:F( { "AI_A2A.Resume:", AIGroup:GetName() } ) - if AIGroup:IsAlive() then - Fsm:__RTB( 0.5 ) - end - -end - ---- @param #AI_A2A self --- @param Wrapper.Group#GROUP AIGroup -function AI_A2A:onafterRefuel( AIGroup, From, Event, To ) - self:F( { AIGroup, From, Event, To } ) - - self:E( "Group " .. self.Controllable:GetName() .. " ... Refuelling! ( " .. self:GetState() .. " )" ) - - if AIGroup and AIGroup:IsAlive() then - local Tanker = GROUP:FindByName( self.TankerName ) - if Tanker:IsAlive() and Tanker:IsAirPlane() then - - local RefuelRoute = {} - - --- Calculate the target route point. - - local CurrentCoord = AIGroup:GetCoordinate() - local ToRefuelCoord = Tanker:GetCoordinate() - local ToRefuelSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) - - --- Create a route point of type air. - local ToRefuelRoutePoint = ToRefuelCoord:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToRefuelSpeed, - true - ) - - self:F( { ToRefuelSpeed = ToRefuelSpeed } ) - - RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint - RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint - - AIGroup:OptionROEHoldFire() - AIGroup:OptionROTEvadeFire() - - local Tasks = {} - Tasks[#Tasks+1] = AIGroup:TaskRefueling() - Tasks[#Tasks+1] = AIGroup:TaskFunction( self:GetClassName() .. ".Resume", self ) - RefuelRoute[#RefuelRoute].task = AIGroup:TaskCombo( Tasks ) - - AIGroup:Route( RefuelRoute, 0.5 ) - else - self:RTB() - end - end - -end - - - ---- @param #AI_A2A self -function AI_A2A:onafterDead() - self:SetStatusOff() -end - - ---- @param #AI_A2A self --- @param Core.Event#EVENTDATA EventData -function AI_A2A:OnCrash( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:E( self.Controllable:GetUnits() ) - if #self.Controllable:GetUnits() == 1 then - self:__Crash( 1, EventData ) - end - end -end - ---- @param #AI_A2A self --- @param Core.Event#EVENTDATA EventData -function AI_A2A:OnEjection( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:__Eject( 1, EventData ) - end -end - ---- @param #AI_A2A self --- @param Core.Event#EVENTDATA EventData -function AI_A2A:OnPilotDead( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:__PilotDead( 1, EventData ) - end -end ---- **AI** -- **Air Patrolling or Staging.** --- --- ![Banner Image](..\Presentations\AI_PATROL\Dia1.JPG) --- --- === --- --- AI PATROL classes makes AI Groups execute an Patrol. --- --- There are the following types of PATROL classes defined: --- --- * @{#AI_A2A_PATROL}: Perform a PATROL in a zone. --- --- ==== --- --- # Demo Missions --- --- ### [AI_PATROL Demo Missions source code](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/PAT%20-%20Patrolling) --- --- ### [AI_PATROL Demo Missions, only for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/PAT%20-%20Patrolling) --- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) --- --- ==== --- --- # YouTube Channel --- --- ### [AI_PATROL YouTube Channel](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl35HvYZKA6G22WMt7iI3zky) --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- ### Contributions: --- --- * **[Dutch_Baron](https://forums.eagle.ru/member.php?u=112075)**: Working together with James has resulted in the creation of the AI_BALANCER class. James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-) --- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Testing and API concept review. --- --- ==== --- --- @module AI_A2A_Patrol - - ---- @type AI_A2A_PATROL --- @extends AI.AI_A2A#AI_A2A - ---- # AI_A2A_PATROL class, extends @{Fsm#FSM_CONTROLLABLE} --- --- The AI_A2A_PATROL class implements the core functions to patrol a @{Zone} by an AI @{Group} or @{Group}. --- --- ![Process](..\Presentations\AI_PATROL\Dia3.JPG) --- --- The AI_A2A_PATROL is assigned a @{Group} and this must be done before the AI_A2A_PATROL process can be started using the **Start** event. --- --- ![Process](..\Presentations\AI_PATROL\Dia4.JPG) --- --- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. --- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- --- ![Process](..\Presentations\AI_PATROL\Dia5.JPG) --- --- This cycle will continue. --- --- ![Process](..\Presentations\AI_PATROL\Dia6.JPG) --- --- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. --- --- ![Process](..\Presentations\AI_PATROL\Dia9.JPG) --- ----- Note that the enemy is not engaged! To model enemy engagement, either tailor the **Detected** event, or --- use derived AI_ classes to model AI offensive or defensive behaviour. --- --- ![Process](..\Presentations\AI_PATROL\Dia10.JPG) --- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. --- --- ![Process](..\Presentations\AI_PATROL\Dia11.JPG) --- --- ## 1. AI_A2A_PATROL constructor --- --- * @{#AI_A2A_PATROL.New}(): Creates a new AI_A2A_PATROL object. --- --- ## 2. AI_A2A_PATROL is a FSM --- --- ![Process](..\Presentations\AI_PATROL\Dia2.JPG) --- --- ### 2.1. AI_A2A_PATROL States --- --- * **None** ( Group ): The process is not started yet. --- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. --- * **Returning** ( Group ): The AI is returning to Base. --- * **Stopped** ( Group ): The process is stopped. --- * **Crashed** ( Group ): The AI has crashed or is dead. --- --- ### 2.2. AI_A2A_PATROL Events --- --- * **Start** ( Group ): Start the process. --- * **Stop** ( Group ): Stop the process. --- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. --- * **RTB** ( Group ): Route the AI to the home base. --- * **Detect** ( Group ): The AI is detecting targets. --- * **Detected** ( Group ): The AI has detected new targets. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- ## 3. Set or Get the AI controllable --- --- * @{#AI_A2A_PATROL.SetControllable}(): Set the AIControllable. --- * @{#AI_A2A_PATROL.GetControllable}(): Get the AIControllable. --- --- ## 4. Set the Speed and Altitude boundaries of the AI controllable --- --- * @{#AI_A2A_PATROL.SetSpeed}(): Set the patrol speed boundaries of the AI, for the next patrol. --- * @{#AI_A2A_PATROL.SetAltitude}(): Set altitude boundaries of the AI, for the next patrol. --- --- ## 5. Manage the detection process of the AI controllable --- --- The detection process of the AI controllable can be manipulated. --- Detection requires an amount of CPU power, which has an impact on your mission performance. --- Only put detection on when absolutely necessary, and the frequency of the detection can also be set. --- --- * @{#AI_A2A_PATROL.SetDetectionOn}(): Set the detection on. The AI will detect for targets. --- * @{#AI_A2A_PATROL.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased. --- --- The detection frequency can be set with @{#AI_A2A_PATROL.SetRefreshTimeInterval}( seconds ), where the amount of seconds specify how much seconds will be waited before the next detection. --- Use the method @{#AI_A2A_PATROL.GetDetectedUnits}() to obtain a list of the @{Unit}s detected by the AI. --- --- The detection can be filtered to potential targets in a specific zone. --- Use the method @{#AI_A2A_PATROL.SetDetectionZone}() to set the zone where targets need to be detected. --- Note that when the zone is too far away, or the AI is not heading towards the zone, or the AI is too high, no targets may be detected --- according the weather conditions. --- --- ## 6. Manage the "out of fuel" in the AI_A2A_PATROL --- --- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. --- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. --- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, --- while a new AI is targetted to the AI_A2A_PATROL. --- Once the time is finished, the old AI will return to the base. --- Use the method @{#AI_A2A_PATROL.ManageFuel}() to have this proces in place. --- --- ## 7. Manage "damage" behaviour of the AI in the AI_A2A_PATROL --- --- When the AI is damaged, it is required that a new Patrol is started. However, damage cannon be foreseen early on. --- Therefore, when the damage treshold is reached, the AI will return immediately to the home base (RTB). --- Use the method @{#AI_A2A_PATROL.ManageDamage}() to have this proces in place. --- --- === --- --- @field #AI_A2A_PATROL -AI_A2A_PATROL = { - ClassName = "AI_A2A_PATROL", -} - ---- Creates a new AI_A2A_PATROL object --- @param #AI_A2A_PATROL self --- @param Wrapper.Group#GROUP AIPatrol --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Group} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Group} in km/h. --- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO --- @return #AI_A2A_PATROL self --- @usage --- -- Define a new AI_A2A_PATROL Object. This PatrolArea will patrol a Group within PatrolZone between 3000 and 6000 meters, with a variying speed between 600 and 900 km/h. --- PatrolZone = ZONE:New( 'PatrolZone' ) --- PatrolSpawn = SPAWN:New( 'Patrol Group' ) --- PatrolArea = AI_A2A_PATROL:New( PatrolZone, 3000, 6000, 600, 900 ) -function AI_A2A_PATROL:New( AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2A:New( AIPatrol ) ) -- #AI_A2A_PATROL - - self.PatrolZone = PatrolZone - self.PatrolFloorAltitude = PatrolFloorAltitude - self.PatrolCeilingAltitude = PatrolCeilingAltitude - self.PatrolMinSpeed = PatrolMinSpeed - self.PatrolMaxSpeed = PatrolMaxSpeed - - -- defafult PatrolAltType to "RADIO" if not specified - self.PatrolAltType = PatrolAltType or "RADIO" - - self:AddTransition( { "Started", "Airborne", "Refuelling" }, "Patrol", "Patrolling" ) - ---- OnBefore Transition Handler for Event Patrol. --- @function [parent=#AI_A2A_PATROL] OnBeforePatrol --- @param #AI_A2A_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Patrol. --- @function [parent=#AI_A2A_PATROL] OnAfterPatrol --- @param #AI_A2A_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Patrol. --- @function [parent=#AI_A2A_PATROL] Patrol --- @param #AI_A2A_PATROL self - ---- Asynchronous Event Trigger for Event Patrol. --- @function [parent=#AI_A2A_PATROL] __Patrol --- @param #AI_A2A_PATROL self --- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Patrolling. --- @function [parent=#AI_A2A_PATROL] OnLeavePatrolling --- @param #AI_A2A_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Patrolling. --- @function [parent=#AI_A2A_PATROL] OnEnterPatrolling --- @param #AI_A2A_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Patrolling", "Route", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_PATROL. - ---- OnBefore Transition Handler for Event Route. --- @function [parent=#AI_A2A_PATROL] OnBeforeRoute --- @param #AI_A2A_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Route. --- @function [parent=#AI_A2A_PATROL] OnAfterRoute --- @param #AI_A2A_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Route. --- @function [parent=#AI_A2A_PATROL] Route --- @param #AI_A2A_PATROL self - ---- Asynchronous Event Trigger for Event Route. --- @function [parent=#AI_A2A_PATROL] __Route --- @param #AI_A2A_PATROL self --- @param #number Delay The delay in seconds. - - - - self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_PATROL. - - return self -end - - - - ---- Sets (modifies) the minimum and maximum speed of the patrol. --- @param #AI_A2A_PATROL self --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Group} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Group} in km/h. --- @return #AI_A2A_PATROL self -function AI_A2A_PATROL:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) - self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) - - self.PatrolMinSpeed = PatrolMinSpeed - self.PatrolMaxSpeed = PatrolMaxSpeed -end - - - ---- Sets the floor and ceiling altitude of the patrol. --- @param #AI_A2A_PATROL self --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @return #AI_A2A_PATROL self -function AI_A2A_PATROL:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) - self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) - - self.PatrolFloorAltitude = PatrolFloorAltitude - self.PatrolCeilingAltitude = PatrolCeilingAltitude -end - - ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. --- @param #AI_A2A_PATROL self --- @return #AI_A2A_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_PATROL:onafterPatrol( AIPatrol, From, Event, To ) - self:F2() - - self:ClearTargetDistance() - - self:__Route( 1 ) - - self.AIPatrol:OnReSpawn( - function( PatrolGroup ) - self:E( "ReSpawn" ) - self:__Reset( 1 ) - self:__Route( 5 ) - end - ) -end - - - ---- @param Wrapper.Group#GROUP AIPatrol --- This statis method is called from the route path within the last task at the last waaypoint of the AIPatrol. --- Note that this method is required, as triggers the next route when patrolling for the AIPatrol. -function AI_A2A_PATROL.PatrolRoute( AIPatrol, Fsm ) - - AIPatrol:F( { "AI_A2A_PATROL.PatrolRoute:", AIPatrol:GetName() } ) - - if AIPatrol:IsAlive() then - Fsm:Route() - end - -end - - ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. --- @param #AI_A2A_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The Group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_PATROL:onafterRoute( AIPatrol, From, Event, To ) - - self:F2() - - -- When RTB, don't allow anymore the routing. - if From == "RTB" then - return - end - - - if AIPatrol:IsAlive() then - - local PatrolRoute = {} - - --- Calculate the target route point. - - local CurrentCoord = AIPatrol:GetCoordinate() - - local ToTargetCoord = self.PatrolZone:GetRandomPointVec2() - ToTargetCoord:SetAlt( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) ) - self:SetTargetDistance( ToTargetCoord ) -- For RTB status check - - local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) - - --- Create a route point of type air. - local ToPatrolRoutePoint = ToTargetCoord:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint - PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint - - local Tasks = {} - Tasks[#Tasks+1] = AIPatrol:TaskFunction( "AI_A2A_PATROL.PatrolRoute", self ) - PatrolRoute[#PatrolRoute].task = AIPatrol:TaskCombo( Tasks ) - - AIPatrol:OptionROEReturnFire() - AIPatrol:OptionROTEvadeFire() - - AIPatrol:Route( PatrolRoute, 0.5 ) - end - -end - ---- @param Wrapper.Group#GROUP AIPatrol -function AI_A2A_PATROL.Resume( AIPatrol ) - - AIPatrol:F( { "AI_A2A_PATROL.Resume:", AIPatrol:GetName() } ) - if AIPatrol:IsAlive() then - local _AI_A2A = AIPatrol:GetState( AIPatrol, "AI_A2A" ) -- #AI_A2A - _AI_A2A:__Reset( 1 ) - _AI_A2A:__Route( 5 ) - end - -end ---- **AI** -- **Execute Combat Air Patrol (CAP).** --- --- ![Banner Image](..\Presentations\AI_CAP\Dia1.JPG) --- --- === --- --- AI CAP classes makes AI Groups execute a Combat Air Patrol. --- --- There are the following types of CAP classes defined: --- --- * @{#AI_A2A_CAP}: Perform a CAP in a zone. --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- * **[Quax](https://forums.eagle.ru/member.php?u=90530)**: Concept, Advice & Testing. --- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Concept, Advice & Testing. --- * **[Gunterlund](http://forums.eagle.ru:8080/member.php?u=75036)**: Test case revision. --- * **[Whisper](http://forums.eagle.ru/member.php?u=3829): Testing. --- * **[Delta99](https://forums.eagle.ru/member.php?u=125166): Testing. --- --- ==== --- --- @module AI_A2A_Cap - ---BASE:TraceClass("AI_A2A_CAP") - ---- @type AI_A2A_CAP --- @extends AI.AI_A2A_Patrol#AI_A2A_PATROL - - ---- # AI_A2A_CAP class, extends @{AI_CAP#AI_PATROL_ZONE} --- --- The AI_A2A_CAP class implements the core functions to patrol a @{Zone} by an AI @{Group} or @{Group} --- and automatically engage any airborne enemies that are within a certain range or within a certain zone. --- --- ![Process](..\Presentations\AI_CAP\Dia3.JPG) --- --- The AI_A2A_CAP is assigned a @{Group} and this must be done before the AI_A2A_CAP process can be started using the **Start** event. --- --- ![Process](..\Presentations\AI_CAP\Dia4.JPG) --- --- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. --- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- --- ![Process](..\Presentations\AI_CAP\Dia5.JPG) --- --- This cycle will continue. --- --- ![Process](..\Presentations\AI_CAP\Dia6.JPG) --- --- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. --- --- ![Process](..\Presentations\AI_CAP\Dia9.JPG) --- --- When enemies are detected, the AI will automatically engage the enemy. --- --- ![Process](..\Presentations\AI_CAP\Dia10.JPG) --- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. --- --- ![Process](..\Presentations\AI_CAP\Dia13.JPG) --- --- ## 1. AI_A2A_CAP constructor --- --- * @{#AI_A2A_CAP.New}(): Creates a new AI_A2A_CAP object. --- --- ## 2. AI_A2A_CAP is a FSM --- --- ![Process](..\Presentations\AI_CAP\Dia2.JPG) --- --- ### 2.1 AI_A2A_CAP States --- --- * **None** ( Group ): The process is not started yet. --- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. --- * **Engaging** ( Group ): The AI is engaging the bogeys. --- * **Returning** ( Group ): The AI is returning to Base.. --- --- ### 2.2 AI_A2A_CAP Events --- --- * **@{AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. --- * **@{AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. --- * **@{#AI_A2A_CAP.Engage}**: Let the AI engage the bogeys. --- * **@{#AI_A2A_CAP.Abort}**: Aborts the engagement and return patrolling in the patrol zone. --- * **@{AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. --- * **@{#AI_A2A_CAP.Destroy}**: The AI has destroyed a bogey @{Unit}. --- * **@{#AI_A2A_CAP.Destroyed}**: The AI has destroyed all bogeys @{Unit}s assigned in the CAS task. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- ## 3. Set the Range of Engagement --- --- ![Range](..\Presentations\AI_CAP\Dia11.JPG) --- --- An optional range can be set in meters, --- that will define when the AI will engage with the detected airborne enemy targets. --- The range can be beyond or smaller than the range of the Patrol Zone. --- The range is applied at the position of the AI. --- Use the method @{AI_CAP#AI_A2A_CAP.SetEngageRange}() to define that range. --- --- ## 4. Set the Zone of Engagement --- --- ![Zone](..\Presentations\AI_CAP\Dia12.JPG) --- --- An optional @{Zone} can be set, --- that will define when the AI will engage with the detected airborne enemy targets. --- Use the method @{AI_Cap#AI_A2A_CAP.SetEngageZone}() to define that Zone. --- --- === --- --- @field #AI_A2A_CAP -AI_A2A_CAP = { - ClassName = "AI_A2A_CAP", -} - ---- Creates a new AI_A2A_CAP object --- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Group} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Group} in km/h. --- @param Dcs.DCSTypes#Speed EngageMinSpeed The minimum speed of the @{Group} in km/h when engaging a target. --- @param Dcs.DCSTypes#Speed EngageMaxSpeed The maximum speed of the @{Group} in km/h when engaging a target. --- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO --- @return #AI_A2A_CAP -function AI_A2A_CAP:New( AICap, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, PatrolAltType ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2A_PATROL:New( AICap, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2A_CAP - - self.Accomplished = false - self.Engaging = false - - self.EngageMinSpeed = EngageMinSpeed - self.EngageMaxSpeed = EngageMaxSpeed - - self:AddTransition( { "Patrolling", "Engaging", "Returning", "Airborne" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_CAP. - - --- OnBefore Transition Handler for Event Engage. - -- @function [parent=#AI_A2A_CAP] OnBeforeEngage - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Engage. - -- @function [parent=#AI_A2A_CAP] OnAfterEngage - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Engage. - -- @function [parent=#AI_A2A_CAP] Engage - -- @param #AI_A2A_CAP self - - --- Asynchronous Event Trigger for Event Engage. - -- @function [parent=#AI_A2A_CAP] __Engage - -- @param #AI_A2A_CAP self - -- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Engaging. --- @function [parent=#AI_A2A_CAP] OnLeaveEngaging --- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Engaging. --- @function [parent=#AI_A2A_CAP] OnEnterEngaging --- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_CAP. - - --- OnBefore Transition Handler for Event Fired. - -- @function [parent=#AI_A2A_CAP] OnBeforeFired - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Fired. - -- @function [parent=#AI_A2A_CAP] OnAfterFired - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Fired. - -- @function [parent=#AI_A2A_CAP] Fired - -- @param #AI_A2A_CAP self - - --- Asynchronous Event Trigger for Event Fired. - -- @function [parent=#AI_A2A_CAP] __Fired - -- @param #AI_A2A_CAP self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_CAP. - - --- OnBefore Transition Handler for Event Destroy. - -- @function [parent=#AI_A2A_CAP] OnBeforeDestroy - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Destroy. - -- @function [parent=#AI_A2A_CAP] OnAfterDestroy - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_A2A_CAP] Destroy - -- @param #AI_A2A_CAP self - - --- Asynchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_A2A_CAP] __Destroy - -- @param #AI_A2A_CAP self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_CAP. - - --- OnBefore Transition Handler for Event Abort. - -- @function [parent=#AI_A2A_CAP] OnBeforeAbort - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Abort. - -- @function [parent=#AI_A2A_CAP] OnAfterAbort - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Abort. - -- @function [parent=#AI_A2A_CAP] Abort - -- @param #AI_A2A_CAP self - - --- Asynchronous Event Trigger for Event Abort. - -- @function [parent=#AI_A2A_CAP] __Abort - -- @param #AI_A2A_CAP self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_CAP. - - --- OnBefore Transition Handler for Event Accomplish. - -- @function [parent=#AI_A2A_CAP] OnBeforeAccomplish - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Accomplish. - -- @function [parent=#AI_A2A_CAP] OnAfterAccomplish - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_A2A_CAP] Accomplish - -- @param #AI_A2A_CAP self - - --- Asynchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_A2A_CAP] __Accomplish - -- @param #AI_A2A_CAP self - -- @param #number Delay The delay in seconds. - - return self -end - ---- onafter State Transition for Event Patrol. --- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AICap The AI Group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_CAP:onafterStart( AICap, From, Event, To ) - - AICap:HandleEvent( EVENTS.Takeoff, nil, self ) - -end - ---- Set the Engage Zone which defines where the AI will engage bogies. --- @param #AI_A2A_CAP self --- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAP. --- @return #AI_A2A_CAP self -function AI_A2A_CAP:SetEngageZone( EngageZone ) - self:F2() - - if EngageZone then - self.EngageZone = EngageZone - else - self.EngageZone = nil - end -end - ---- Set the Engage Range when the AI will engage with airborne enemies. --- @param #AI_A2A_CAP self --- @param #number EngageRange The Engage Range. --- @return #AI_A2A_CAP self -function AI_A2A_CAP:SetEngageRange( EngageRange ) - self:F2() - - if EngageRange then - self.EngageRange = EngageRange - else - self.EngageRange = nil - end -end - ---- onafter State Transition for Event Patrol. --- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap The AI Group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_CAP:onafterPatrol( AICap, From, Event, To ) - - -- Call the parent Start event handler - self:GetParent(self).onafterPatrol( self, AICap, From, Event, To ) - self:HandleEvent( EVENTS.Dead ) - -end - --- todo: need to fix this global function - ---- @param Wrapper.Group#GROUP AICap -function AI_A2A_CAP.AttackRoute( AICap, Fsm ) - - AICap:F( { "AI_A2A_CAP.AttackRoute:", AICap:GetName() } ) - - if AICap:IsAlive() then - Fsm:__Engage( 0.5 ) - end -end - ---- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_CAP:onbeforeEngage( AICap, From, Event, To ) - - if self.Accomplished == true then - return false - end -end - ---- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap The AI Group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_CAP:onafterAbort( AICap, From, Event, To ) - AICap:ClearTasks() - self:__Route( 0.5 ) -end - - ---- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap The AICap Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_CAP:onafterEngage( AICap, From, Event, To, AttackSetUnit ) - - self:F( { AICap, From, Event, To, AttackSetUnit} ) - - self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT - - local FirstAttackUnit = self.AttackSetUnit:GetFirst() -- Wrapper.Unit#UNIT - - if FirstAttackUnit and FirstAttackUnit:IsAlive() then -- If there is no attacker anymore, stop the engagement. - - if AICap:IsAlive() then - - local EngageRoute = {} - - --- Calculate the target route point. - local CurrentCoord = AICap:GetCoordinate() - local ToTargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate() - local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) - local ToInterceptAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) ) - - --- Create a route point of type air. - local ToPatrolRoutePoint = CurrentCoord:Translate( 5000, ToInterceptAngle ):WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - self:F( { Angle = ToInterceptAngle, ToTargetSpeed = ToTargetSpeed } ) - self:T2( { self.MinSpeed, self.MaxSpeed, ToTargetSpeed } ) - - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint - - local AttackTasks = {} - - for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do - local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT - self:T( { "Attacking Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } ) - if AttackUnit:IsAlive() and AttackUnit:IsAir() then - AttackTasks[#AttackTasks+1] = AICap:TaskAttackUnit( AttackUnit ) - end - end - - if #AttackTasks == 0 then - self:E("No targets found -> Going back to Patrolling") - self:__Abort( 0.5 ) - else - AICap:OptionROEOpenFire() - AICap:OptionROTEvadeFire() - - AttackTasks[#AttackTasks+1] = AICap:TaskFunction( "AI_A2A_CAP.AttackRoute", self ) - EngageRoute[#EngageRoute].task = AICap:TaskCombo( AttackTasks ) - end - - AICap:Route( EngageRoute, 0.5 ) - end - else - self:E("No targets found -> Going back to Patrolling") - self:__Abort( 0.5 ) - end -end - ---- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_CAP:onafterAccomplish( AICap, From, Event, To ) - self.Accomplished = true - self:SetDetectionOff() -end - ---- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @param Core.Event#EVENTDATA EventData -function AI_A2A_CAP:onafterDestroy( AICap, From, Event, To, EventData ) - - if EventData.IniUnit then - self.AttackUnits[EventData.IniUnit] = nil - end -end - ---- @param #AI_A2A_CAP self --- @param Core.Event#EVENTDATA EventData -function AI_A2A_CAP:OnEventDead( EventData ) - self:F( { "EventDead", EventData } ) - - if EventData.IniDCSUnit then - if self.AttackUnits and self.AttackUnits[EventData.IniUnit] then - self:__Destroy( 1, EventData ) - end - end -end - ---- @param Wrapper.Group#GROUP AICap -function AI_A2A_CAP.Resume( AICap ) - - AICap:F( { "AI_A2A_CAP.Resume:", AICap:GetName() } ) - if AICap:IsAlive() then - local _AI_A2A = AICap:GetState( AICap, "AI_A2A" ) -- #AI_A2A - _AI_A2A:__Reset( 1 ) - _AI_A2A:__Route( 5 ) - end - -end ---- **AI** -- **Execute Ground Controlled Interception (GCI).** --- --- ![Banner Image](..\Presentations\AI_GCI\Dia1.JPG) --- --- === --- --- AI A2A_INTEREPT class makes AI Groups execute an Intercept. --- --- There are the following types of GCI classes defined: --- --- * @{#AI_A2A_GCI}: Perform a GCI in a zone. --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- ==== --- --- @module AI_A2A_GCI - - ---BASE:TraceClass("AI_A2A_GCI") - - ---- @type AI_A2A_GCI --- @extends AI.AI_A2A#AI_A2A - - ---- # AI_A2A_GCI class, extends @{AI_A2A#AI_A2A} --- --- The AI_A2A_GCI class implements the core functions to intercept intruders. The Engage function will intercept intruders. --- --- ![Process](..\Presentations\AI_GCI\Dia3.JPG) --- --- The AI_A2A_GCI is assigned a @{Group} and this must be done before the AI_A2A_GCI process can be started using the **Start** event. --- --- ![Process](..\Presentations\AI_GCI\Dia4.JPG) --- --- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. --- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- --- ![Process](..\Presentations\AI_GCI\Dia5.JPG) --- --- This cycle will continue. --- --- ![Process](..\Presentations\AI_GCI\Dia6.JPG) --- --- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. --- --- ![Process](..\Presentations\AI_GCI\Dia9.JPG) --- --- When enemies are detected, the AI will automatically engage the enemy. --- --- ![Process](..\Presentations\AI_GCI\Dia10.JPG) --- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. --- --- ![Process](..\Presentations\AI_GCI\Dia13.JPG) --- --- ## 1. AI_A2A_GCI constructor --- --- * @{#AI_A2A_GCI.New}(): Creates a new AI_A2A_GCI object. --- --- ## 2. AI_A2A_GCI is a FSM --- --- ![Process](..\Presentations\AI_GCI\Dia2.JPG) --- --- ### 2.1 AI_A2A_GCI States --- --- * **None** ( Group ): The process is not started yet. --- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. --- * **Engaging** ( Group ): The AI is engaging the bogeys. --- * **Returning** ( Group ): The AI is returning to Base.. --- --- ### 2.2 AI_A2A_GCI Events --- --- * **@{AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. --- * **@{AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. --- * **@{#AI_A2A_GCI.Engage}**: Let the AI engage the bogeys. --- * **@{#AI_A2A_GCI.Abort}**: Aborts the engagement and return patrolling in the patrol zone. --- * **@{AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. --- * **@{#AI_A2A_GCI.Destroy}**: The AI has destroyed a bogey @{Unit}. --- * **@{#AI_A2A_GCI.Destroyed}**: The AI has destroyed all bogeys @{Unit}s assigned in the CAS task. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- ## 3. Set the Range of Engagement --- --- ![Range](..\Presentations\AI_GCI\Dia11.JPG) --- --- An optional range can be set in meters, --- that will define when the AI will engage with the detected airborne enemy targets. --- The range can be beyond or smaller than the range of the Patrol Zone. --- The range is applied at the position of the AI. --- Use the method @{AI_GCI#AI_A2A_GCI.SetEngageRange}() to define that range. --- --- ## 4. Set the Zone of Engagement --- --- ![Zone](..\Presentations\AI_GCI\Dia12.JPG) --- --- An optional @{Zone} can be set, --- that will define when the AI will engage with the detected airborne enemy targets. --- Use the method @{AI_Cap#AI_A2A_GCI.SetEngageZone}() to define that Zone. --- --- === --- --- @field #AI_A2A_GCI -AI_A2A_GCI = { - ClassName = "AI_A2A_GCI", -} - - - ---- Creates a new AI_A2A_GCI object --- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept --- @return #AI_A2A_GCI -function AI_A2A_GCI:New( AIIntercept, EngageMinSpeed, EngageMaxSpeed ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2A:New( AIIntercept ) ) -- #AI_A2A_GCI - - self.Accomplished = false - self.Engaging = false - - self.EngageMinSpeed = EngageMinSpeed - self.EngageMaxSpeed = EngageMaxSpeed - self.PatrolMinSpeed = EngageMinSpeed - self.PatrolMaxSpeed = EngageMaxSpeed - - self.PatrolAltType = "RADIO" - - self:AddTransition( { "Started", "Engaging", "Returning", "Airborne" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_GCI. - - --- OnBefore Transition Handler for Event Engage. - -- @function [parent=#AI_A2A_GCI] OnBeforeEngage - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Engage. - -- @function [parent=#AI_A2A_GCI] OnAfterEngage - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Engage. - -- @function [parent=#AI_A2A_GCI] Engage - -- @param #AI_A2A_GCI self - - --- Asynchronous Event Trigger for Event Engage. - -- @function [parent=#AI_A2A_GCI] __Engage - -- @param #AI_A2A_GCI self - -- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Engaging. --- @function [parent=#AI_A2A_GCI] OnLeaveEngaging --- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Engaging. --- @function [parent=#AI_A2A_GCI] OnEnterEngaging --- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_GCI. - - --- OnBefore Transition Handler for Event Fired. - -- @function [parent=#AI_A2A_GCI] OnBeforeFired - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Fired. - -- @function [parent=#AI_A2A_GCI] OnAfterFired - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Fired. - -- @function [parent=#AI_A2A_GCI] Fired - -- @param #AI_A2A_GCI self - - --- Asynchronous Event Trigger for Event Fired. - -- @function [parent=#AI_A2A_GCI] __Fired - -- @param #AI_A2A_GCI self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_GCI. - - --- OnBefore Transition Handler for Event Destroy. - -- @function [parent=#AI_A2A_GCI] OnBeforeDestroy - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Destroy. - -- @function [parent=#AI_A2A_GCI] OnAfterDestroy - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_A2A_GCI] Destroy - -- @param #AI_A2A_GCI self - - --- Asynchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_A2A_GCI] __Destroy - -- @param #AI_A2A_GCI self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_GCI. - - --- OnBefore Transition Handler for Event Abort. - -- @function [parent=#AI_A2A_GCI] OnBeforeAbort - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Abort. - -- @function [parent=#AI_A2A_GCI] OnAfterAbort - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Abort. - -- @function [parent=#AI_A2A_GCI] Abort - -- @param #AI_A2A_GCI self - - --- Asynchronous Event Trigger for Event Abort. - -- @function [parent=#AI_A2A_GCI] __Abort - -- @param #AI_A2A_GCI self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_GCI. - - --- OnBefore Transition Handler for Event Accomplish. - -- @function [parent=#AI_A2A_GCI] OnBeforeAccomplish - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Accomplish. - -- @function [parent=#AI_A2A_GCI] OnAfterAccomplish - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_A2A_GCI] Accomplish - -- @param #AI_A2A_GCI self - - --- Asynchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_A2A_GCI] __Accomplish - -- @param #AI_A2A_GCI self - -- @param #number Delay The delay in seconds. - - return self -end - ---- onafter State Transition for Event Patrol. --- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept The AI Group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_GCI:onafterStart( AIIntercept, From, Event, To ) - - AIIntercept:HandleEvent( EVENTS.Takeoff, nil, self ) - -end - - - ---- onafter State Transition for Event Patrol. --- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept The AI Group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_GCI:onafterEngage( AIIntercept, From, Event, To ) - - self:HandleEvent( EVENTS.Dead ) - -end - --- todo: need to fix this global function - ---- @param Wrapper.Group#GROUP AIControllable -function AI_A2A_GCI.InterceptRoute( AIIntercept, Fsm ) - - AIIntercept:F( { "AI_A2A_GCI.InterceptRoute:", AIIntercept:GetName() } ) - - if AIIntercept:IsAlive() then - Fsm:__Engage( 0.5 ) - - --local Task = AIIntercept:TaskOrbitCircle( 4000, 400 ) - --AIIntercept:SetTask( Task ) - end -end - ---- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_GCI:onbeforeEngage( AIIntercept, From, Event, To ) - - if self.Accomplished == true then - return false - end -end - ---- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept The AI Group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_GCI:onafterAbort( AIIntercept, From, Event, To ) - AIIntercept:ClearTasks() - self:Return() - self:__RTB( 0.5 ) -end - - ---- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept The GroupGroup managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_GCI:onafterEngage( AIIntercept, From, Event, To, AttackSetUnit ) - - self:F( { AIIntercept, From, Event, To, AttackSetUnit} ) - - self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT - - local FirstAttackUnit = self.AttackSetUnit:GetFirst() - - if FirstAttackUnit and FirstAttackUnit:IsAlive() then - - if AIIntercept:IsAlive() then - - local EngageRoute = {} - - local CurrentCoord = AIIntercept:GetCoordinate() - - --- Calculate the target route point. - - local CurrentCoord = AIIntercept:GetCoordinate() - - local ToTargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate() - self:SetTargetDistance( ToTargetCoord ) -- For RTB status check - - local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) - local ToInterceptAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) ) - - --- Create a route point of type air. - local ToPatrolRoutePoint = CurrentCoord:Translate( 15000, ToInterceptAngle ):WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - self:F( { Angle = ToInterceptAngle, ToTargetSpeed = ToTargetSpeed } ) - self:F( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } ) - - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint - - local AttackTasks = {} - - for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do - local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT - if AttackUnit:IsAlive() and AttackUnit:IsAir() then - self:T( { "Intercepting Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } ) - AttackTasks[#AttackTasks+1] = AIIntercept:TaskAttackUnit( AttackUnit ) - end - end - - if #AttackTasks == 0 then - self:E("No targets found -> Going RTB") - self:Return() - self:__RTB( 0.5 ) - else - AIIntercept:OptionROEOpenFire() - AIIntercept:OptionROTEvadeFire() - - AttackTasks[#AttackTasks+1] = AIIntercept:TaskFunction( "AI_A2A_GCI.InterceptRoute", self ) - EngageRoute[#EngageRoute].task = AIIntercept:TaskCombo( AttackTasks ) - end - - AIIntercept:Route( EngageRoute, 0.5 ) - - end - else - self:E("No targets found -> Going RTB") - self:Return() - self:__RTB( 0.5 ) - end -end - ---- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_GCI:onafterAccomplish( AIIntercept, From, Event, To ) - self.Accomplished = true - self:SetDetectionOff() -end - ---- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @param Core.Event#EVENTDATA EventData -function AI_A2A_GCI:onafterDestroy( AIIntercept, From, Event, To, EventData ) - - if EventData.IniUnit then - self.AttackUnits[EventData.IniUnit] = nil - end -end - ---- @param #AI_A2A_GCI self --- @param Core.Event#EVENTDATA EventData -function AI_A2A_GCI:OnEventDead( EventData ) - self:F( { "EventDead", EventData } ) - - if EventData.IniDCSUnit then - if self.AttackUnits and self.AttackUnits[EventData.IniUnit] then - self:__Destroy( 1, EventData ) - end - end -end ---- **AI** - The AI_A2A_DISPATCHER creates an automatic A2A defense system based on an EWR network targets and coordinating CAP and GCI. --- --- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia1.JPG) --- --- ==== --- --- # QUICK START GUIDE --- --- There are basically two classes available to model an A2A defense system. --- --- AI\_A2A\_DISPATCHER is the main A2A defense class that models the A2A defense system. --- AI\_A2A\_GCICAP derives or inherits from AI\_A2A\_DISPATCHER and is a more **noob** user friendly class, but is less flexible. --- --- Before you start using the AI\_A2A\_DISPATCHER or AI\_A2A\_GCICAP ask youself the following questions. --- --- ## 0. Do I need AI\_A2A\_DISPATCHER or do I need AI\_A2A\_GCICAP? --- --- AI\_A2A\_GCICAP, automates a lot of the below questions using the mission editor and requires minimal lua scripting. --- But the AI\_A2A\_GCICAP provides less flexibility and a lot of options are defaulted. --- With AI\_A2A\_DISPATCHER you can setup a much more **fine grained** A2A defense mechanism, but some more (easy) lua scripting is required. --- --- ## 1. Which Coalition am I modeling an A2A defense system for? blue or red? --- --- One AI\_A2A\_DISPATCHER object can create a defense system for **one coalition**, which is blue or red. --- If you want to create a **mutual defense system**, for both blue and red, then you need to create **two** AI\_A2A\_DISPATCHER **objects**, --- each governing their defense system. --- --- --- ## 2. Which type of EWR will I setup? Grouping based per AREA, per TYPE or per UNIT? (Later others will follow). --- --- The MOOSE framework leverages the @{Detection} classes to perform the EWR detection. --- Several types of @{Detection} classes exist, and the most common characteristics of these classes is that they: --- --- * Perform detections from multiple FACs as one co-operating entity. --- * Communicate with a Head Quarters, which consolidates each detection. --- * Groups detections based on a method (per area, per type or per unit). --- * Communicates detections. --- --- ## 3. Which EWR units will be used as part of the detection system? Only Ground or also Airborne? --- --- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. --- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US). --- Additionally, ANY other radar capable unit can be part of the EWR network! Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar. --- The position of these units is very important as they need to provide enough coverage --- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them. --- --- ## 4. Is a border required? --- --- Is this a cold car or a hot war situation? In case of a cold war situation, a border can be set that will only trigger defenses --- if the border is crossed by enemy units. --- --- ## 5. What maximum range needs to be checked to allow defenses to engage any attacker? --- --- A good functioning defense will have a "maximum range" evaluated to the enemy when CAP will be engaged or GCI will be spawned. --- --- ## 6. Which Airbases, Carrier Ships, Farps will take part in the defense system for the Coalition? --- --- Carefully plan which airbases will take part in the coalition. Color each airbase in the color of the coalition. --- --- ## 7. Which Squadrons will I create and which name will I give each Squadron? --- --- The defense system works with Squadrons. Each Squadron must be given a unique name, that forms the **key** to the defense system. --- Several options and activities can be set per Squadron. --- --- ## 8. Where will the Squadrons be located? On Airbases? On Carrier Ships? On Farps? --- --- Squadrons are placed as the "home base" on an airfield, carrier or farp. --- Carefully plan where each Squadron will be located as part of the defense system. --- --- ## 9. Which plane models will I assign for each Squadron? Do I need one plane model or more plane models per squadron? --- --- Per Squadron, one or multiple plane models can be allocated as **Templates**. --- These are late activated groups with one airplane or helicopter that start with a specific name, called the **template prefix**. --- The A2A defense system will select from the given templates a random template to spawn a new plane (group). --- --- ## 10. Which payloads, skills and skins will these plane models have? --- --- Per Squadron, even if you have one plane model, you can still allocate multiple templates of one plane model, --- each having different payloads, skills and skins. --- The A2A defense system will select from the given templates a random template to spawn a new plane (group). --- --- ## 11. For each Squadron, which will perform CAP? --- --- Per Squadron, evaluate which Squadrons will perform CAP. --- Not all Squadrons need to perform CAP. --- --- ## 12. For each Squadron doing CAP, in which ZONE(s) will the CAP be performed? --- --- Per CAP, evaluate **where** the CAP will be performed, in other words, define the **zone**. --- Near the border or a bit further away? --- --- ## 13. For each Squadron doing CAP, which zone types will I create? --- --- Per CAP zone, evaluate whether you want: --- --- * simple trigger zones --- * polygon zones --- * moving zones --- --- Depending on the type of zone selected, a different @{Zone} object needs to be created from a ZONE_ class. --- --- ## 14. For each Squadron doing CAP, what are the time intervals and CAP amounts to be performed? --- --- For each CAP: --- --- * **How many** CAP you want to have airborne at the same time? --- * **How frequent** you want the defense mechanism to check whether to start a new CAP? --- --- ## 15. For each Squadron, which will perform GCI? --- --- For each Squadron, evaluate which Squadrons will perform GCI? --- Not all Squadrons need to perform GCI. --- --- ## 16. For each Squadron, which takeoff method will I use? --- --- For each Squadron, evaluate which takeoff method will be used: --- --- * Straight from the air --- * From the runway --- * From a parking spot with running engines --- * From a parking spot with cold engines --- --- **The default takeoff method is staight in the air.** --- --- ## 17. For each Squadron, which landing method will I use? --- --- For each Squadron, evaluate which landing method will be used: --- --- * Despawn near the airbase when returning --- * Despawn after landing on the runway --- * Despawn after engine shutdown after landing --- --- **The default landing method is despawn when near the airbase when returning.** --- --- ## 18. For each Squadron, which overhead will I use? --- --- For each Squadron, depending on the airplane type (modern, old) and payload, which overhead is required to provide any defense? --- In other words, if **X** attacker airplanes are detected, how many **Y** defense airplanes need to be spawned per squadron? --- The **Y** is dependent on the type of airplane (era), payload, fuel levels, skills etc. --- The overhead is a **factor** that will calculate dynamically how many **Y** defenses will be required based on **X** attackers detected. --- --- **The default overhead is 1. A value greater than 1, like 1.5 will increase the overhead with 50%, a value smaller than 1, like 0.5 will decrease the overhead with 50%.** --- --- ## 19. For each Squadron, which grouping will I use? --- --- When multiple targets are detected, how will defense airplanes be grouped when multiple defense airplanes are spawned for multiple attackers? --- Per one, two, three, four? --- --- **The default grouping is 1. That means, that each spawned defender will act individually.** --- --- === --- --- ### Authors: **Sven Van de Velde (FlightControl)** rework of GCICAP + introduction of new concepts (squadrons). --- ### Authors: **Stonehouse**, **SNAFU** in terms of the advice, documentation, and the original GCICAP script. --- --- @module AI_A2A_Dispatcher - - - -do -- AI_A2A_DISPATCHER - - --- AI_A2A_DISPATCHER class. - -- @type AI_A2A_DISPATCHER - -- @extends Tasking.DetectionManager#DETECTION_MANAGER - - --- # AI\_A2A\_DISPATCHER class, extends @{Tasking#DETECTION_MANAGER} - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia1.JPG) - -- - -- The @{#AI_A2A_DISPATCHER} class is designed to create an automatic air defence system for a coalition. - -- - -- ==== - -- - -- # Demo Missions - -- - -- ### [AI\_A2A\_DISPATCHER Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching) - -- - -- ==== - -- - -- # YouTube Channel - -- - -- ### [DCS WORLD - MOOSE - A2A GCICAP - Build an automatic A2A Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) - -- - -- === - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia3.JPG) - -- - -- It includes automatic spawning of Combat Air Patrol aircraft (CAP) and Ground Controlled Intercept aircraft (GCI) in response to enemy air movements that are detected by a ground based radar network. - -- CAP flights will take off and proceed to designated CAP zones where they will remain on station until the ground radars direct them to intercept detected enemy aircraft or they run short of fuel and must return to base (RTB). When a CAP flight leaves their zone to perform an interception or return to base a new CAP flight will spawn to take their place. - -- If all CAP flights are engaged or RTB then additional GCI interceptors will scramble to intercept unengaged enemy aircraft under ground radar control. - -- With a little time and with a little work it provides the mission designer with a convincing and completely automatic air defence system. - -- In short it is a plug in very flexible and configurable air defence module for DCS World. - -- - -- Note that in order to create a two way A2A defense system, two AI\_A2A\_DISPATCHER defense system may need to be created, for each coalition one. - -- This is a good implementation, because maybe in the future, more coalitions may become available in DCS world. - -- - -- === - -- - -- # USAGE GUIDE - -- - -- ## 1. AI\_A2A\_DISPATCHER constructor: - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_1.JPG) - -- - -- - -- The @{#AI_A2A_DISPATCHER.New}() method creates a new AI\_A2A\_DISPATCHER instance. - -- - -- ### 1.1. Define the **EWR network**: - -- - -- As part of the AI\_A2A\_DISPATCHER :New() constructor, an EWR network must be given as the first parameter. - -- An EWR network, or, Early Warning Radar network, is used to early detect potential airborne targets and to understand the position of patrolling targets of the enemy. - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia5.JPG) - -- - -- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. - -- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US). - -- Additionally, ANY other radar capable unit can be part of the EWR network! Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar. - -- The position of these units is very important as they need to provide enough coverage - -- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them. - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia7.JPG) - -- - -- Additionally in a hot war situation where the border is no longer respected the placement of radars has a big effect on how fast the war escalates. - -- For example if they are a long way forward and can detect enemy planes on the ground and taking off - -- they will start to vector CAP and GCI flights to attack them straight away which will immediately draw a response from the other coalition. - -- Having the radars further back will mean a slower escalation because fewer targets will be detected and - -- therefore less CAP and GCI flights will spawn and this will tend to make just the border area active rather than a melee over the whole map. - -- It all depends on what the desired effect is. - -- - -- EWR networks are **dynamically constructed**, that is, they form part of the @{Functional#DETECTION_BASE} object that is given as the input parameter of the AI\_A2A\_DISPATCHER class. - -- By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network, - -- increasing or decreasing the radar coverage of the Early Warning System. - -- - -- See the following example to setup an EWR network containing EWR stations and AWACS. - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_2.JPG) - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_3.JPG) - -- - -- -- Define a SET_GROUP object that builds a collection of groups that define the EWR network. - -- -- Here we build the network with all the groups that have a name starting with DF CCCP AWACS and DF CCCP EWR. - -- DetectionSetGroup = SET_GROUP:New() - -- DetectionSetGroup:FilterPrefixes( { "DF CCCP AWACS", "DF CCCP EWR" } ) - -- DetectionSetGroup:FilterStart() - -- - -- -- Setup the detection and group targets to a 30km range! - -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 ) - -- - -- -- Setup the A2A dispatcher, and initialize it. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- - -- The above example creates a SET_GROUP instance, and stores this in the variable (object) **DetectionSetGroup**. - -- **DetectionSetGroup** is then being configured to filter all active groups with a group name starting with **DF CCCP AWACS** or **DF CCCP EWR** to be included in the Set. - -- **DetectionSetGroup** is then being ordered to start the dynamic filtering. Note that any destroy or new spawn of a group with the above names will be removed or added to the Set. - -- - -- Then a new Detection object is created from the class DETECTION_AREAS. A grouping radius of 30000 is choosen, which is 30km. - -- The **Detection** object is then passed to the @{#AI_A2A_DISPATCHER.New}() method to indicate the EWR network configuration and setup the A2A defense detection mechanism. - -- - -- You could build a **mutual defense system** like this: - -- - -- A2ADispatcher_Red = AI_A2A_DISPATCHER:New( EWR_Red ) - -- A2ADispatcher_Blue = AI_A2A_DISPATCHER:New( EWR_Blue ) - -- - -- ### 2. Define the detected **target grouping radius**: - -- - -- The target grouping radius is a property of the Detection object, that was passed to the AI\_A2A\_DISPATCHER object, but can be changed. - -- The grouping radius should not be too small, but also depends on the types of planes and the era of the simulation. - -- Fast planes like in the 80s, need a larger radius than WWII planes. - -- Typically I suggest to use 30000 for new generation planes and 10000 for older era aircraft. - -- - -- Note that detected targets are constantly re-grouped, that is, when certain detected aircraft are moving further than the group radius, then these aircraft will become a separate - -- group being detected. This may result in additional GCI being started by the dispatcher! So don't make this value too small! - -- - -- ## 3. Set the **Engage Radius**: - -- - -- Define the **Engage Radius** to **engage any target by airborne friendlies**, - -- which are executing **cap** or **returning** from an intercept mission. - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia10.JPG) - -- - -- If there is a target area detected and reported, - -- then any friendlies that are airborne near this target area, - -- will be commanded to (re-)engage that target when available (if no other tasks were commanded). - -- - -- For example, if **50000** or **50km** is given as a value, then any friendly that is airborne within **50km** from the detected target, - -- will be considered to receive the command to engage that target area. - -- - -- You need to evaluate the value of this parameter carefully: - -- - -- * If too small, more intercept missions may be triggered upon detected target areas. - -- * If too large, any airborne cap may not be able to reach the detected target area in time, because it is too far. - -- - -- The **default** Engage Radius is defined as **100000** or **100km**. - -- Use the method @{#AI_A2A_DISPATCHER.SetEngageRadius}() to set a specific Engage Radius. - -- **The Engage Radius is defined for ALL squadrons which are operational.** - -- - -- Demonstration Mission: [AID-019 - AI_A2A - Engage Range Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-019%20-%20AI_A2A%20-%20Engage%20Range%20Test) - -- - -- In this example an Engage Radius is set to various values. - -- - -- -- Set 50km as the radius to engage any target by airborne friendlies. - -- A2ADispatcher:SetEngageRadius( 50000 ) - -- - -- -- Set 100km as the radius to engage any target by airborne friendlies. - -- A2ADispatcher:SetEngageRadius() -- 100000 is the default value. - -- - -- - -- ## 4. Set the **Ground Controlled Intercept Radius** or **Gci radius**: - -- - -- When targets are detected that are still really far off, you don't want the AI_A2A_DISPATCHER to launch intercepts just yet. - -- You want it to wait until a certain Gci range is reached, which is the **distance of the closest airbase to target** - -- being **smaller** than the **Ground Controlled Intercept radius** or **Gci radius**. - -- - -- The **default** Gci radius is defined as **200000** or **200km**. Override the default Gci radius when the era of the warfare is early, or, - -- when you don't want to let the AI_A2A_DISPATCHER react immediately when a certain border or area is not being crossed. - -- - -- Use the method @{#AI_A2A_DISPATCHER.SetGciRadius}() to set a specific controlled ground intercept radius. - -- **The Ground Controlled Intercept radius is defined for ALL squadrons which are operational.** - -- - -- Demonstration Mission: [AID-013 - AI_A2A - Intercept Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-013%20-%20AI_A2A%20-%20Intercept%20Test) - -- - -- In these examples, the Gci Radius is set to various values: - -- - -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- - -- -- Set 100km as the radius to ground control intercept detected targets from the nearest airbase. - -- A2ADispatcher:SetGciRadius( 100000 ) - -- - -- -- Set 200km as the radius to ground control intercept. - -- A2ADispatcher:SetGciRadius() -- 200000 is the default value. - -- - -- ## 5. Set the **borders**: - -- - -- According to the tactical and strategic design of the mission broadly decide the shape and extent of red and blue territories. - -- They should be laid out such that a border area is created between the two coalitions. - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia4.JPG) - -- - -- **Define a border area to simulate a cold war scenario.** - -- Use the method @{#AI_A2A_DISPATCHER.SetBorderZone}() to create a border zone for the dispatcher. - -- - -- A **cold war** is one where CAP aircraft patrol their territory but will not attack enemy aircraft or launch GCI aircraft unless enemy aircraft enter their territory. In other words the EWR may detect an enemy aircraft but will only send aircraft to attack it if it crosses the border. - -- A **hot war** is one where CAP aircraft will intercept any detected enemy aircraft and GCI aircraft will launch against detected enemy aircraft without regard for territory. In other words if the ground radar can detect the enemy aircraft then it will send CAP and GCI aircraft to attack it. - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia9.JPG) - -- - -- If it’s a cold war then the **borders of red and blue territory** need to be defined using a @{zone} object derived from @{Zone#ZONE_BASE}. - -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than - -- it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. - -- In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. - -- - -- Demonstration Mission: [AID-009 - AI_A2A - Border Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-009 - AI_A2A - Border Test) - -- - -- In this example a border is set for the CCCP A2A dispatcher: - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_4.JPG) - -- - -- -- Setup the A2A dispatcher, and initialize it. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- - -- -- Setup the border. - -- -- Initialize the dispatcher, setting up a border zone. This is a polygon, - -- -- which takes the waypoints of a late activated group with the name CCCP Border as the boundaries of the border area. - -- -- Any enemy crossing this border will be engaged. - -- - -- CCCPBorderZone = ZONE_POLYGON:New( "CCCP Border", GROUP:FindByName( "CCCP Border" ) ) - -- A2ADispatcher:SetBorderZone( CCCPBorderZone ) - -- - -- ## 6. Squadrons: - -- - -- The AI\_A2A\_DISPATCHER works with **Squadrons**, that need to be defined using the different methods available. - -- - -- Use the method @{#AI_A2A_DISPATCHER.SetSquadron}() to **setup a new squadron** active at an airfield, - -- while defining which plane types are being used by the squadron and how many resources are available. - -- - -- Squadrons: - -- - -- * Have name (string) that is the identifier or key of the squadron. - -- * Have specific plane types. - -- * Are located at one airbase. - -- * Optionally have a limited set of resources. The default is that squadrons have **unlimited resources**. - -- - -- The name of the squadron given acts as the **squadron key** in the AI\_A2A\_DISPATCHER:Squadron...() methods. - -- - -- Additionally, squadrons have specific configuration options to: - -- - -- * Control how new aircraft are taking off from the airfield (in the air, cold, hot, at the runway). - -- * Control how returning aircraft are landing at the airfield (in the air near the airbase, after landing, after engine shutdown). - -- * Control the **grouping** of new aircraft spawned at the airfield. If there is more than one aircraft to be spawned, these may be grouped. - -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of planes and amount of resources, the mission designer can choose to increase or reduce the amount of planes spawned. - -- - -- For performance and bug workaround reasons within DCS, squadrons have different methods to spawn new aircraft or land returning or damaged aircraft. - -- - -- This example defines a couple of squadrons. Note the templates defined within the Mission Editor. - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_5.JPG) - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_6.JPG) - -- - -- -- Setup the squadrons. - -- A2ADispatcher:SetSquadron( "Mineralnye", AIRBASE.Caucasus.Mineralnye_Vody, { "SQ CCCP SU-27" }, 20 ) - -- A2ADispatcher:SetSquadron( "Maykop", AIRBASE.Caucasus.Maykop_Khanskaya, { "SQ CCCP MIG-31" }, 20 ) - -- A2ADispatcher:SetSquadron( "Mozdok", AIRBASE.Caucasus.Mozdok, { "SQ CCCP MIG-31" }, 20 ) - -- A2ADispatcher:SetSquadron( "Sochi", AIRBASE.Caucasus.Sochi_Adler, { "SQ CCCP SU-27" }, 20 ) - -- A2ADispatcher:SetSquadron( "Novo", AIRBASE.Caucasus.Novorossiysk, { "SQ CCCP SU-27" }, 20 ) - -- - -- ### 6.1. Set squadron take-off methods - -- - -- Use the various SetSquadronTakeoff... methods to control how squadrons are taking-off from the airfield: - -- - -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoff}() is the generic configuration method to control takeoff from the air, hot, cold or from the runway. See the method for further details. - -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAir}() will spawn new aircraft from the squadron directly in the air. - -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingCold}() will spawn new aircraft in without running engines at a parking spot at the airfield. - -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield. - -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromRunway}() will spawn new aircraft at the runway at the airfield. - -- - -- **The default landing method is to spawn new aircraft directly in the air.** - -- - -- Use these methods to fine-tune for specific airfields that are known to create bottlenecks, or have reduced airbase efficiency. - -- The more and the longer aircraft need to taxi at an airfield, the more risk there is that: - -- - -- * aircraft will stop waiting for each other or for a landing aircraft before takeoff. - -- * aircraft may get into a "dead-lock" situation, where two aircraft are blocking each other. - -- * aircraft may collide at the airbase. - -- * aircraft may be awaiting the landing of a plane currently in the air, but never lands ... - -- - -- Currently within the DCS engine, the airfield traffic coordination is erroneous and contains a lot of bugs. - -- If you experience while testing problems with aircraft take-off or landing, please use one of the above methods as a solution to workaround these issues! - -- - -- This example sets the default takeoff method to be from the runway. - -- And for a couple of squadrons overrides this default method. - -- - -- -- Setup the Takeoff methods - -- - -- -- The default takeoff - -- A2ADispatcher:SetDefaultTakeOffFromRunway() - -- - -- -- The individual takeoff per squadron - -- A2ADispatcher:SetSquadronTakeoff( "Mineralnye", AI_A2A_DISPATCHER.Takeoff.Air ) - -- A2ADispatcher:SetSquadronTakeoffInAir( "Sochi" ) - -- A2ADispatcher:SetSquadronTakeoffFromRunway( "Mozdok" ) - -- A2ADispatcher:SetSquadronTakeoffFromParkingCold( "Maykop" ) - -- A2ADispatcher:SetSquadronTakeoffFromParkingHot( "Novo" ) - -- - -- - -- ### 6.1. Set Squadron takeoff altitude when spawning new aircraft in the air. - -- - -- In the case of the @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAir}() there is also an other parameter that can be applied. - -- That is modifying or setting the **altitude** from where planes spawn in the air. - -- Use the method @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAirAltitude}() to set the altitude for a specific squadron. - -- The default takeoff altitude can be modified or set using the method @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAirAltitude}(). - -- As part of the method @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAir}() a parameter can be specified to set the takeoff altitude. - -- If this parameter is not specified, then the default altitude will be used for the squadron. - -- - -- ### 6.2. Set squadron landing methods - -- - -- In analogy with takeoff, the landing methods are to control how squadrons land at the airfield: - -- - -- * @{#AI_A2A_DISPATCHER.SetSquadronLanding}() is the generic configuration method to control landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. - -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingNearAirbase}() will despawn the returning aircraft in the air when near the airfield. - -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway. - -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtEngineShutdown}() will despawn the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. - -- - -- You can use these methods to minimize the airbase coodination overhead and to increase the airbase efficiency. - -- When there are lots of aircraft returning for landing, at the same airbase, the takeoff process will be halted, which can cause a complete failure of the - -- A2A defense system, as no new CAP or GCI planes can takeoff. - -- Note that the method @{#AI_A2A_DISPATCHER.SetSquadronLandingNearAirbase}() will only work for returning aircraft, not for damaged or out of fuel aircraft. - -- Damaged or out-of-fuel aircraft are returning to the nearest friendly airbase and will land, and are out of control from ground control. - -- - -- This example defines the default landing method to be at the runway. - -- And for a couple of squadrons overrides this default method. - -- - -- -- Setup the Landing methods - -- - -- -- The default landing method - -- A2ADispatcher:SetDefaultLandingAtRunway() - -- - -- -- The individual landing per squadron - -- A2ADispatcher:SetSquadronLandingAtRunway( "Mineralnye" ) - -- A2ADispatcher:SetSquadronLandingNearAirbase( "Sochi" ) - -- A2ADispatcher:SetSquadronLandingAtEngineShutdown( "Mozdok" ) - -- A2ADispatcher:SetSquadronLandingNearAirbase( "Maykop" ) - -- A2ADispatcher:SetSquadronLanding( "Novo", AI_A2A_DISPATCHER.Landing.AtRunway ) - -- - -- - -- ### 6.3. Set squadron grouping - -- - -- Use the method @{#AI_A2A_DISPATCHER.SetSquadronGrouping}() to set the grouping of CAP or GCI flights that will take-off when spawned. - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia12.JPG) - -- - -- In the case of GCI, the @{#AI_A2A_DISPATCHER.SetSquadronGrouping}() method has additional behaviour. When there aren't enough CAP flights airborne, a GCI will be initiated for the remaining - -- targets to be engaged. Depending on the grouping parameter, the spawned flights for GCI are grouped into this setting. - -- For example with a group setting of 2, if 3 targets are detected and cannot be engaged by CAP or any airborne flight, - -- a GCI needs to be started, the GCI flights will be grouped as follows: Group 1 of 2 flights and Group 2 of one flight! - -- - -- Even more ... If one target has been detected, and the overhead is 1.5, grouping is 1, then two groups of planes will be spawned, with one unit each! - -- - -- The **grouping value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense flights grouping when the tactical situation changes. - -- - -- ### 6.4. Overhead and Balance the effectiveness of the air defenses in case of GCI. - -- - -- The effectiveness can be set with the **overhead parameter**. This is a number that is used to calculate the amount of Units that dispatching command will allocate to GCI in surplus of detected amount of units. - -- The **default value** of the overhead parameter is 1.0, which means **equal balance**. - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia11.JPG) - -- - -- However, depending on the (type of) aircraft (strength and payload) in the squadron and the amount of resources available, this parameter can be changed. - -- - -- The @{#AI_A2A_DISPATCHER.SetSquadronOverhead}() method can be used to tweak the defense strength, - -- taking into account the plane types of the squadron. - -- - -- For example, a MIG-31 with full long-distance A2A missiles payload, may still be less effective than a F-15C with short missiles... - -- So in this case, one may want to use the @{#AI_A2A_DISPATCHER.SetOverhead}() method to allocate more defending planes as the amount of detected attacking planes. - -- The overhead must be given as a decimal value with 1 as the neutral value, which means that overhead values: - -- - -- * Higher than 1.0, for example 1.5, will increase the defense unit amounts. For 4 planes detected, 6 planes will be spawned. - -- * Lower than 1, for example 0.75, will decrease the defense unit amounts. For 4 planes detected, only 3 planes will be spawned. - -- - -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group - -- multiplied by the Overhead and rounded up to the smallest integer. - -- - -- For example ... If one target has been detected, and the overhead is 1.5, grouping is 1, then two groups of planes will be spawned, with one unit each! - -- - -- The **overhead value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense overhead when the tactical situation changes. - -- - -- ## 6.5. Squadron fuel treshold. - -- - -- When an airplane gets **out of fuel** to a certain %-tage, which is by default **15% (0.15)**, there are two possible actions that can be taken: - -- - The defender will go RTB, and will be replaced with a new defender if possible. - -- - The defender will refuel at a tanker, if a tanker has been specified for the squadron. - -- - -- Use the method @{#AI_A2A_DISPATCHER.SetSquadronFuelThreshold}() to set the **squadron fuel treshold** of spawned airplanes for all squadrons. - -- - -- ## 7. Setup a squadron for CAP - -- - -- ### 7.1. Set the CAP zones - -- - -- CAP zones are patrol areas where Combat Air Patrol (CAP) flights loiter until they either return to base due to low fuel or are assigned an interception task by ground control. - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia6.JPG) - -- - -- * As the CAP flights wander around within the zone waiting to be tasked, these zones need to be large enough that the aircraft are not constantly turning - -- but do not have to be big and numerous enough to completely cover a border. - -- - -- * CAP zones can be of any type, and are derived from the @{Zone#ZONE_BASE} class. Zones can be @{Zone#ZONE}, @{Zone#ZONE_POLYGON}, @{Zone#ZONE_UNIT}, @{Zone#ZONE_GROUP}, etc. - -- This allows to setup **static, moving and/or complex zones** wherein aircraft will perform the CAP. - -- - -- * Typically 20000-50000 metres width is used and they are spaced so that aircraft in the zone waiting for tasks don’t have to far to travel to protect their coalitions important targets. - -- These targets are chosen as part of the mission design and might be an important airfield or town etc. - -- Zone size is also determined somewhat by territory size, plane types - -- (eg WW2 aircraft might mean smaller zones or more zones because they are slower and take longer to intercept enemy aircraft). - -- - -- * In a **cold war** it is important to make sure a CAP zone doesn’t intrude into enemy territory as otherwise CAP flights will likely cross borders - -- and spark a full scale conflict which will escalate rapidly. - -- - -- * CAP flights do not need to be in the CAP zone before they are “on station” and ready for tasking. - -- - -- * Typically if a CAP flight is tasked and therefore leaves their zone empty while they go off and intercept their target another CAP flight will spawn to take their place. - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia7.JPG) - -- - -- The following example illustrates how CAP zones are coded: - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_8.JPG) - -- - -- -- CAP Squadron execution. - -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) - -- A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 ) - -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_7.JPG) - -- - -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) - -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) - -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_9.JPG) - -- - -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") - -- A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) - -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- - -- Note the different @{Zone} MOOSE classes being used to create zones of different types. Please click the @{Zone} link for more information about the different zone types. - -- Zones can be circles, can be setup in the mission editor using trigger zones, but can also be setup in the mission editor as polygons and in this case GROUP objects are being used! - -- - -- ## 7.2. Set the squadron to execute CAP: - -- - -- The method @{#AI_A2A_DISPATCHER.SetSquadronCap}() defines a CAP execution for a squadron. - -- - -- Setting-up a CAP zone also requires specific parameters: - -- - -- * The minimum and maximum altitude - -- * The minimum speed and maximum patrol speed - -- * The minimum and maximum engage speed - -- * The type of altitude measurement - -- - -- These define how the squadron will perform the CAP while partrolling. Different terrain types requires different types of CAP. - -- - -- The @{#AI_A2A_DISPATCHER.SetSquadronCapInterval}() method specifies **how much** and **when** CAP flights will takeoff. - -- - -- It is recommended not to overload the air defense with CAP flights, as these will decrease the performance of the overall system. - -- - -- For example, the following setup will create a CAP for squadron "Sochi": - -- - -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) - -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- - -- ## 7.3. Squadron tanker to refuel when executing CAP and defender is out of fuel. - -- - -- Instead of sending CAP to RTB when out of fuel, you can let CAP refuel in mid air using a tanker. - -- This greatly increases the efficiency of your CAP operations. - -- - -- In the mission editor, setup a group with task Refuelling. A tanker unit of the correct coalition will be automatically selected. - -- Then, use the method @{#AI_A2A_DISPATCHER.SetDefaultTanker}() to set the default tanker for the refuelling. - -- You can also specify a specific tanker for refuelling for a squadron by using the method @{#AI_A2A_DISPATCHER.SetSquadronTanker}(). - -- - -- When the tanker specified is alive and in the air, the tanker will be used for refuelling. - -- - -- For example, the following setup will create a CAP for squadron "Gelend" with a refuel task for the squadron: - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_10.JPG) - -- - -- -- Define the CAP - -- A2ADispatcher:SetSquadron( "Gelend", AIRBASE.Caucasus.Gelendzhik, { "SQ CCCP SU-30" }, 20 ) - -- A2ADispatcher:SetSquadronCap( "Gelend", ZONE:New( "PatrolZoneGelend" ), 4000, 8000, 600, 800, 1000, 1300 ) - -- A2ADispatcher:SetSquadronCapInterval( "Gelend", 2, 30, 600, 1 ) - -- A2ADispatcher:SetSquadronGci( "Gelend", 900, 1200 ) - -- - -- -- Setup the Refuelling for squadron "Gelend", at tanker (group) "TankerGelend" when the fuel in the tank of the CAP defenders is less than 80%. - -- A2ADispatcher:SetSquadronFuelThreshold( "Gelend", 0.8 ) - -- A2ADispatcher:SetSquadronTanker( "Gelend", "TankerGelend" ) - -- - -- ## 8. Setup a squadron for GCI: - -- - -- The method @{#AI_A2A_DISPATCHER.SetSquadronGci}() defines a GCI execution for a squadron. - -- - -- Setting-up a GCI readiness also requires specific parameters: - -- - -- * The minimum speed and maximum patrol speed - -- - -- Essentially this controls how many flights of GCI aircraft can be active at any time. - -- Note allowing large numbers of active GCI flights can adversely impact mission performance on low or medium specification hosts/servers. - -- GCI needs to be setup at strategic airbases. Too far will mean that the aircraft need to fly a long way to reach the intruders, - -- too short will mean that the intruders may have alraedy passed the ideal interception point! - -- - -- For example, the following setup will create a GCI for squadron "Sochi": - -- - -- A2ADispatcher:SetSquadronGci( "Mozdok", 900, 1200 ) - -- - -- ## 9. Other configuration options - -- - -- ### 9.1. Set a tactical display panel: - -- - -- Every 30 seconds, a tactical display panel can be shown that illustrates what the status is of the different groups controlled by AI\_A2A\_DISPATCHER. - -- Use the method @{#AI_A2A_DISPATCHER.SetTacticalDisplay}() to switch on the tactical display panel. The default will not show this panel. - -- Note that there may be some performance impact if this panel is shown. - -- - -- ## 10. Defaults settings. - -- - -- This provides a good overview of the different parameters that are setup or hardcoded by default. - -- For some default settings, a method is available that allows you to tweak the defaults. - -- - -- ## 10.1. Default takeoff method. - -- - -- The default **takeoff method** is set to **in the air**, which means that new spawned airplanes will be spawned directly in the air above the airbase by default. - -- - -- **The default takeoff method can be set for ALL squadrons that don't have an individual takeoff method configured.** - -- - -- * @{#AI_A2A_DISPATCHER.SetDefaultTakeoff}() is the generic configuration method to control takeoff by default from the air, hot, cold or from the runway. See the method for further details. - -- * @{#AI_A2A_DISPATCHER.SetDefaultTakeoffInAir}() will spawn by default new aircraft from the squadron directly in the air. - -- * @{#AI_A2A_DISPATCHER.SetDefaultTakeoffFromParkingCold}() will spawn by default new aircraft in without running engines at a parking spot at the airfield. - -- * @{#AI_A2A_DISPATCHER.SetDefaultTakeoffFromParkingHot}() will spawn by default new aircraft in with running engines at a parking spot at the airfield. - -- * @{#AI_A2A_DISPATCHER.SetDefaultTakeoffFromRunway}() will spawn by default new aircraft at the runway at the airfield. - -- - -- ## 10.2. Default landing method. - -- - -- The default **landing method** is set to **near the airbase**, which means that returning airplanes will be despawned directly in the air by default. - -- - -- The default landing method can be set for ALL squadrons that don't have an individual landing method configured. - -- - -- * @{#AI_A2A_DISPATCHER.SetDefaultLanding}() is the generic configuration method to control by default landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. - -- * @{#AI_A2A_DISPATCHER.SetDefaultLandingNearAirbase}() will despawn by default the returning aircraft in the air when near the airfield. - -- * @{#AI_A2A_DISPATCHER.SetDefaultLandingAtRunway}() will despawn by default the returning aircraft directly after landing at the runway. - -- * @{#AI_A2A_DISPATCHER.SetDefaultLandingAtEngineShutdown}() will despawn by default the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. - -- - -- ## 10.3. Default overhead. - -- - -- The default **overhead** is set to **1**. That essentially means that there isn't any overhead set by default. - -- - -- The default overhead value can be set for ALL squadrons that don't have an individual overhead value configured. - -- - -- Use the @{#AI_A2A_DISPATCHER.SetDefaultOverhead}() method can be used to set the default overhead or defense strength for ALL squadrons. - -- - -- ## 10.4. Default grouping. - -- - -- The default **grouping** is set to **one airplane**. That essentially means that there won't be any grouping applied by default. - -- - -- The default grouping value can be set for ALL squadrons that don't have an individual grouping value configured. - -- - -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultGrouping}() to set the **default grouping** of spawned airplanes for all squadrons. - -- - -- ## 10.5. Default RTB fuel treshold. - -- - -- When an airplane gets **out of fuel** to a certain %-tage, which is **15% (0.15)**, it will go RTB, and will be replaced with a new airplane when applicable. - -- - -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultFuelThreshold}() to set the **default fuel treshold** of spawned airplanes for all squadrons. - -- - -- ## 10.6. Default RTB damage treshold. - -- - -- When an airplane is **damaged** to a certain %-tage, which is **40% (0.40)**, it will go RTB, and will be replaced with a new airplane when applicable. - -- - -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultDamageThreshold}() to set the **default damage treshold** of spawned airplanes for all squadrons. - -- - -- ## 10.7. Default settings for CAP. - -- - -- ### 10.7.1. Default CAP Time Interval. - -- - -- CAP is time driven, and will evaluate in random time intervals if a new CAP needs to be spawned. - -- The **default CAP time interval** is between **180** and **600** seconds. - -- - -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultCapTimeInterval}() to set the **default CAP time interval** of spawned airplanes for all squadrons. - -- Note that you can still change the CAP limit and CAP time intervals for each CAP individually using the @{#AI_A2A_DISPATCHER.SetSquadronCapTimeInterval}() method. - -- - -- ### 10.7.2. Default CAP limit. - -- - -- Multiple CAP can be airborne at the same time for one squadron, which is controlled by the **CAP limit**. - -- The **default CAP limit** is 1 CAP per squadron to be airborne at the same time. - -- Note that the default CAP limit is used when a Squadron CAP is defined, and cannot be changed afterwards. - -- So, ensure that you set the default CAP limit **before** you spawn the Squadron CAP. - -- - -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultCapTimeInterval}() to set the **default CAP time interval** of spawned airplanes for all squadrons. - -- Note that you can still change the CAP limit and CAP time intervals for each CAP individually using the @{#AI_A2A_DISPATCHER.SetSquadronCapTimeInterval}() method. - -- - -- ## 10.7.3. Default tanker for refuelling when executing CAP. - -- - -- Instead of sending CAP to RTB when out of fuel, you can let CAP refuel in mid air using a tanker. - -- This greatly increases the efficiency of your CAP operations. - -- - -- In the mission editor, setup a group with task Refuelling. A tanker unit of the correct coalition will be automatically selected. - -- Then, use the method @{#AI_A2A_DISPATCHER.SetDefaultTanker}() to set the tanker for the dispatcher. - -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultFuelTreshold}() to set the %-tage left in the defender airplane tanks when a refuel action is needed. - -- - -- When the tanker specified is alive and in the air, the tanker will be used for refuelling. - -- - -- For example, the following setup will set the default refuel tanker to "Tanker": - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_11.JPG) - -- - -- -- Define the CAP - -- A2ADispatcher:SetSquadron( "Sochi", AIRBASE.Caucasus.Sochi_Adler, { "SQ CCCP SU-34" }, 20 ) - -- A2ADispatcher:SetSquadronCap( "Sochi", ZONE:New( "PatrolZone" ), 4000, 8000, 600, 800, 1000, 1300 ) - -- A2ADispatcher:SetSquadronCapInterval("Sochi", 2, 30, 600, 1 ) - -- A2ADispatcher:SetSquadronGci( "Sochi", 900, 1200 ) - -- - -- -- Set the default tanker for refuelling to "Tanker", when the default fuel treshold has reached 90% fuel left. - -- A2ADispatcher:SetDefaultFuelThreshold( 0.9 ) - -- A2ADispatcher:SetDefaultTanker( "Tanker" ) - -- - -- ## 10.8. Default settings for GCI. - -- - -- ## 10.8.1. Optimal intercept point calculation. - -- - -- When intruders are detected, the intrusion path of the attackers can be monitored by the EWR. - -- Although defender planes might be on standby at the airbase, it can still take some time to get the defenses up in the air if there aren't any defenses airborne. - -- This time can easily take 2 to 3 minutes, and even then the defenders still need to fly towards the target, which takes also time. - -- - -- Therefore, an optimal **intercept point** is calculated which takes a couple of parameters: - -- - -- * The average bearing of the intruders for an amount of seconds. - -- * The average speed of the intruders for an amount of seconds. - -- * An assumed time it takes to get planes operational at the airbase. - -- - -- The **intercept point** will determine: - -- - -- * If there are any friendlies close to engage the target. These can be defenders performing CAP or defenders in RTB. - -- * The optimal airbase from where defenders will takeoff for GCI. - -- - -- Use the method @{#AI_A2A_DISPATCHER.SetIntercept}() to modify the assumed intercept delay time to calculate a valid interception. - -- - -- ## 10.8.2. Default Disengage Radius. - -- - -- The radius to **disengage any target** when the **distance** of the defender to the **home base** is larger than the specified meters. - -- The default Disengage Radius is **300km** (300000 meters). Note that the Disengage Radius is applicable to ALL squadrons! - -- - -- Use the method @{#AI_A2A_DISPATCHER.SetDisengageRadius}() to modify the default Disengage Radius to another distance setting. - -- - -- - -- ## 11. Q & A: - -- - -- ### 11.1. Which countries will be selected for each coalition? - -- - -- Which countries are assigned to a coalition influences which units are available to the coalition. - -- For example because the mission calls for a EWR radar on the blue side the Ukraine might be chosen as a blue country - -- so that the 55G6 EWR radar unit is available to blue. - -- Some countries assign different tasking to aircraft, for example Germany assigns the CAP task to F-4E Phantoms but the USA does not. - -- Therefore if F4s are wanted as a coalition’s CAP or GCI aircraft Germany will need to be assigned to that coalition. - -- - -- ### 11.2. Country, type, load out, skill and skins for CAP and GCI aircraft? - -- - -- * Note these can be from any countries within the coalition but must be an aircraft with one of the main tasks being “CAP”. - -- * Obviously skins which are selected must be available to all players that join the mission otherwise they will see a default skin. - -- * Load outs should be appropriate to a CAP mission eg perhaps drop tanks for CAP flights and extra missiles for GCI flights. - -- * These decisions will eventually lead to template aircraft units being placed as late activation units that the script will use as templates for spawning CAP and GCI flights. Up to 4 different aircraft configurations can be chosen for each coalition. The spawned aircraft will inherit the characteristics of the template aircraft. - -- * The selected aircraft type must be able to perform the CAP tasking for the chosen country. - -- - -- - -- @field #AI_A2A_DISPATCHER - AI_A2A_DISPATCHER = { - ClassName = "AI_A2A_DISPATCHER", - Detection = nil, - } - - - --- Enumerator for spawns at airbases - -- @type AI_A2A_DISPATCHER.Takeoff - -- @extends Wrapper.Group#GROUP.Takeoff - - --- @field #AI_A2A_DISPATCHER.Takeoff Takeoff - AI_A2A_DISPATCHER.Takeoff = GROUP.Takeoff - - --- Defnes Landing location. - -- @field Landing - AI_A2A_DISPATCHER.Landing = { - NearAirbase = 1, - AtRunway = 2, - AtEngineShutdown = 3, - } - - --- AI_A2A_DISPATCHER constructor. - -- This is defining the A2A DISPATCHER for one coaliton. - -- The Dispatcher works with a @{Functional#Detection} object that is taking of the detection of targets using the EWR units. - -- The Detection object is polymorphic, depending on the type of detection object choosen, the detection will work differently. - -- @param #AI_A2A_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE Detection The DETECTION object that will detects targets using the the Early Warning Radar network. - -- @return #AI_A2A_DISPATCHER self - -- @usage - -- - -- -- Setup the Detection, using DETECTION_AREAS. - -- -- First define the SET of GROUPs that are defining the EWR network. - -- -- Here with prefixes DF CCCP AWACS, DF CCCP EWR. - -- DetectionSetGroup = SET_GROUP:New() - -- DetectionSetGroup:FilterPrefixes( { "DF CCCP AWACS", "DF CCCP EWR" } ) - -- DetectionSetGroup:FilterStart() - -- - -- -- Define the DETECTION_AREAS, using the DetectionSetGroup, with a 30km grouping radius. - -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 ) - -- - -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) -- - -- - function AI_A2A_DISPATCHER:New( Detection ) - - -- Inherits from DETECTION_MANAGER - local self = BASE:Inherit( self, DETECTION_MANAGER:New( nil, Detection ) ) -- #AI_A2A_DISPATCHER - - self.Detection = Detection -- Functional.Detection#DETECTION_AREAS - - -- This table models the DefenderSquadron templates. - self.DefenderSquadrons = {} -- The Defender Squadrons. - self.DefenderSpawns = {} - self.DefenderTasks = {} -- The Defenders Tasks. - self.DefenderDefault = {} -- The Defender Default Settings over all Squadrons. - - -- TODO: Check detection through radar. - self.Detection:FilterCategories( { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) - --self.Detection:InitDetectRadar( true ) - self.Detection:SetRefreshTimeInterval( 30 ) - - self:SetEngageRadius() - self:SetGciRadius() - self:SetIntercept( 300 ) -- A default intercept delay time of 300 seconds. - self:SetDisengageRadius( 300000 ) -- The default Disengage Radius is 300 km. - - self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Air ) - self:SetDefaultTakeoffInAirAltitude( 500 ) -- Default takeoff is 500 meters above the ground. - self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.NearAirbase ) - self:SetDefaultOverhead( 1 ) - self:SetDefaultGrouping( 1 ) - self:SetDefaultFuelThreshold( 0.15, 0 ) -- 15% of fuel remaining in the tank will trigger the airplane to return to base or refuel. - self:SetDefaultDamageThreshold( 0.4 ) -- When 40% of damage, go RTB. - self:SetDefaultCapTimeInterval( 180, 600 ) -- Between 180 and 600 seconds. - self:SetDefaultCapLimit( 1 ) -- Maximum one CAP per squadron. - - - self:AddTransition( "Started", "Assign", "Started" ) - - --- OnAfter Transition Handler for Event Assign. - -- @function [parent=#AI_A2A_DISPATCHER] OnAfterAssign - -- @param #AI_A2A_DISPATCHER self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param Tasking.Task_A2A#AI_A2A Task - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param #string PlayerName - - self:AddTransition( "*", "CAP", "*" ) - - --- CAP Handler OnBefore for AI_A2A_DISPATCHER - -- @function [parent=#AI_A2A_DISPATCHER] OnBeforeCAP - -- @param #AI_A2A_DISPATCHER self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- CAP Handler OnAfter for AI_A2A_DISPATCHER - -- @function [parent=#AI_A2A_DISPATCHER] OnAfterCAP - -- @param #AI_A2A_DISPATCHER self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- CAP Trigger for AI_A2A_DISPATCHER - -- @function [parent=#AI_A2A_DISPATCHER] CAP - -- @param #AI_A2A_DISPATCHER self - - --- CAP Asynchronous Trigger for AI_A2A_DISPATCHER - -- @function [parent=#AI_A2A_DISPATCHER] __CAP - -- @param #AI_A2A_DISPATCHER self - -- @param #number Delay - - self:AddTransition( "*", "GCI", "*" ) - - --- GCI Handler OnBefore for AI_A2A_DISPATCHER - -- @function [parent=#AI_A2A_DISPATCHER] OnBeforeGCI - -- @param #AI_A2A_DISPATCHER self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- GCI Handler OnAfter for AI_A2A_DISPATCHER - -- @function [parent=#AI_A2A_DISPATCHER] OnAfterGCI - -- @param #AI_A2A_DISPATCHER self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- GCI Trigger for AI_A2A_DISPATCHER - -- @function [parent=#AI_A2A_DISPATCHER] GCI - -- @param #AI_A2A_DISPATCHER self - - --- GCI Asynchronous Trigger for AI_A2A_DISPATCHER - -- @function [parent=#AI_A2A_DISPATCHER] __GCI - -- @param #AI_A2A_DISPATCHER self - -- @param #number Delay - - self:AddTransition( "*", "ENGAGE", "*" ) - - --- ENGAGE Handler OnBefore for AI_A2A_DISPATCHER - -- @function [parent=#AI_A2A_DISPATCHER] OnBeforeENGAGE - -- @param #AI_A2A_DISPATCHER self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- ENGAGE Handler OnAfter for AI_A2A_DISPATCHER - -- @function [parent=#AI_A2A_DISPATCHER] OnAfterENGAGE - -- @param #AI_A2A_DISPATCHER self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- ENGAGE Trigger for AI_A2A_DISPATCHER - -- @function [parent=#AI_A2A_DISPATCHER] ENGAGE - -- @param #AI_A2A_DISPATCHER self - - --- ENGAGE Asynchronous Trigger for AI_A2A_DISPATCHER - -- @function [parent=#AI_A2A_DISPATCHER] __ENGAGE - -- @param #AI_A2A_DISPATCHER self - -- @param #number Delay - - - -- Subscribe to the CRASH event so that when planes are shot - -- by a Unit from the dispatcher, they will be removed from the detection... - -- This will avoid the detection to still "know" the shot unit until the next detection. - -- Otherwise, a new intercept or engage may happen for an already shot plane! - - - self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead ) - self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead ) - - self:HandleEvent( EVENTS.Land ) - self:HandleEvent( EVENTS.EngineShutdown ) - - self:SetTacticalDisplay( false ) - - self:__Start( 5 ) - - return self - end - - --- @param #AI_A2A_DISPATCHER self - -- @param Core.Event#EVENTDATA EventData - function AI_A2A_DISPATCHER:OnEventCrashOrDead( EventData ) - self.Detection:ForgetDetectedUnit( EventData.IniUnitName ) - end - - --- @param #AI_A2A_DISPATCHER self - -- @param Core.Event#EVENTDATA EventData - function AI_A2A_DISPATCHER:OnEventLand( EventData ) - self:E( "Landed" ) - local DefenderUnit = EventData.IniUnit - local Defender = EventData.IniGroup - local Squadron = self:GetSquadronFromDefender( Defender ) - if Squadron then - self:F( { SquadronName = Squadron.Name } ) - local LandingMethod = self:GetSquadronLanding( Squadron.Name ) - if LandingMethod == AI_A2A_DISPATCHER.Landing.AtRunway then - local DefenderSize = Defender:GetSize() - if DefenderSize == 1 then - self:RemoveDefenderFromSquadron( Squadron, Defender ) - end - DefenderUnit:Destroy() - return - end - if DefenderUnit:GetLife() ~= DefenderUnit:GetLife0() then - -- Damaged units cannot be repaired anymore. - DefenderUnit:Destroy() - return - end - if DefenderUnit:GetFuel() <= self.DefenderDefault.FuelThreshold then - DefenderUnit:Destroy() - return - end - end - end - - --- @param #AI_A2A_DISPATCHER self - -- @param Core.Event#EVENTDATA EventData - function AI_A2A_DISPATCHER:OnEventEngineShutdown( EventData ) - local DefenderUnit = EventData.IniUnit - local Defender = EventData.IniGroup - local Squadron = self:GetSquadronFromDefender( Defender ) - if Squadron then - self:F( { SquadronName = Squadron.Name } ) - local LandingMethod = self:GetSquadronLanding( Squadron.Name ) - if LandingMethod == AI_A2A_DISPATCHER.Landing.AtEngineShutdown then - local DefenderSize = Defender:GetSize() - if DefenderSize == 1 then - self:RemoveDefenderFromSquadron( Squadron, Defender ) - end - DefenderUnit:Destroy() - end - end - end - - --- Define the radius to engage any target by airborne friendlies, which are executing cap or returning from an intercept mission. - -- If there is a target area detected and reported, then any friendlies that are airborne near this target area, - -- will be commanded to (re-)engage that target when available (if no other tasks were commanded). - -- - -- For example, if 100000 is given as a value, then any friendly that is airborne within 100km from the detected target, - -- will be considered to receive the command to engage that target area. - -- - -- You need to evaluate the value of this parameter carefully: - -- - -- * If too small, more intercept missions may be triggered upon detected target areas. - -- * If too large, any airborne cap may not be able to reach the detected target area in time, because it is too far. - -- - -- **Use the method @{#AI_A2A_DISPATCHER.SetEngageRadius}() to modify the default Engage Radius for ALL squadrons.** - -- - -- Demonstration Mission: [AID-019 - AI_A2A - Engage Range Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-019%20-%20AI_A2A%20-%20Engage%20Range%20Test) - -- - -- @param #AI_A2A_DISPATCHER self - -- @param #number EngageRadius (Optional, Default = 100000) The radius to report friendlies near the target. - -- @return #AI_A2A_DISPATCHER - -- @usage - -- - -- -- Set 50km as the radius to engage any target by airborne friendlies. - -- A2ADispatcher:SetEngageRadius( 50000 ) - -- - -- -- Set 100km as the radius to engage any target by airborne friendlies. - -- A2ADispatcher:SetEngageRadius() -- 100000 is the default value. - -- - function AI_A2A_DISPATCHER:SetEngageRadius( EngageRadius ) - - self.Detection:SetFriendliesRange( EngageRadius or 100000 ) - - return self - end - - --- Define the radius to disengage any target when the distance to the home base is larger than the specified meters. - -- @param #AI_A2A_DISPATCHER self - -- @param #number DisengageRadius (Optional, Default = 300000) The radius to disengage a target when too far from the home base. - -- @return #AI_A2A_DISPATCHER - -- @usage - -- - -- -- Set 50km as the Disengage Radius. - -- A2ADispatcher:SetDisengageRadius( 50000 ) - -- - -- -- Set 100km as the Disengage Radius. - -- A2ADispatcher:SetDisngageRadius() -- 300000 is the default value. - -- - function AI_A2A_DISPATCHER:SetDisengageRadius( DisengageRadius ) - - self.DisengageRadius = DisengageRadius or 300000 - - return self - end - - - --- Define the radius to check if a target can be engaged by an ground controlled intercept. - -- When targets are detected that are still really far off, you don't want the AI_A2A_DISPATCHER to launch intercepts just yet. - -- You want it to wait until a certain Gci range is reached, which is the **distance of the closest airbase to target** - -- being **smaller** than the **Ground Controlled Intercept radius** or **Gci radius**. - -- - -- The **default** Gci radius is defined as **200000** or **200km**. Override the default Gci radius when the era of the warfare is early, or, - -- when you don't want to let the AI_A2A_DISPATCHER react immediately when a certain border or area is not being crossed. - -- - -- Use the method @{#AI_A2A_DISPATCHER.SetGciRadius}() to set a specific controlled ground intercept radius. - -- **The Ground Controlled Intercept radius is defined for ALL squadrons which are operational.** - -- - -- Demonstration Mission: [AID-013 - AI_A2A - Intercept Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-013%20-%20AI_A2A%20-%20Intercept%20Test) - -- - -- @param #AI_A2A_DISPATCHER self - -- @param #number GciRadius (Optional, Default = 200000) The radius to ground control intercept detected targets from the nearest airbase. - -- @return #AI_A2A_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- - -- -- Set 100km as the radius to ground control intercept detected targets from the nearest airbase. - -- A2ADispatcher:SetGciRadius( 100000 ) - -- - -- -- Set 200km as the radius to ground control intercept. - -- A2ADispatcher:SetGciRadius() -- 200000 is the default value. - -- - function AI_A2A_DISPATCHER:SetGciRadius( GciRadius ) - - self.GciRadius = GciRadius or 200000 - - return self - end - - - - --- Define a border area to simulate a **cold war** scenario. - -- A **cold war** is one where CAP aircraft patrol their territory but will not attack enemy aircraft or launch GCI aircraft unless enemy aircraft enter their territory. In other words the EWR may detect an enemy aircraft but will only send aircraft to attack it if it crosses the border. - -- A **hot war** is one where CAP aircraft will intercept any detected enemy aircraft and GCI aircraft will launch against detected enemy aircraft without regard for territory. In other words if the ground radar can detect the enemy aircraft then it will send CAP and GCI aircraft to attack it. - -- If it’s a cold war then the **borders of red and blue territory** need to be defined using a @{zone} object derived from @{Zone#ZONE_BASE}. This method needs to be used for this. - -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. Set the noborders parameter to 1 - -- @param #AI_A2A_DISPATCHER self - -- @param Core.Zone#ZONE_BASE BorderZone An object derived from ZONE_BASE, or a list of objects derived from ZONE_BASE. - -- @return #AI_A2A_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- - -- -- Set one ZONE_POLYGON object as the border for the A2A dispatcher. - -- local BorderZone = ZONE_POLYGON( "CCCP Border", GROUP:FindByName( "CCCP Border" ) ) -- The GROUP object is a late activate helicopter unit. - -- A2ADispatcher:SetBorderZone( BorderZone ) - -- - -- or - -- - -- -- Set two ZONE_POLYGON objects as the border for the A2A dispatcher. - -- local BorderZone1 = ZONE_POLYGON( "CCCP Border1", GROUP:FindByName( "CCCP Border1" ) ) -- The GROUP object is a late activate helicopter unit. - -- local BorderZone2 = ZONE_POLYGON( "CCCP Border2", GROUP:FindByName( "CCCP Border2" ) ) -- The GROUP object is a late activate helicopter unit. - -- A2ADispatcher:SetBorderZone( { BorderZone1, BorderZone2 } ) - -- - -- - function AI_A2A_DISPATCHER:SetBorderZone( BorderZone ) - - self.Detection:SetAcceptZones( BorderZone ) - - return self - end - - --- Display a tactical report every 30 seconds about which aircraft are: - -- * Patrolling - -- * Engaging - -- * Returning - -- * Damaged - -- * Out of Fuel - -- * ... - -- @param #AI_A2A_DISPATCHER self - -- @param #boolean TacticalDisplay Provide a value of **true** to display every 30 seconds a tactical overview. - -- @return #AI_A2A_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- - -- -- Now Setup the Tactical Display for debug mode. - -- A2ADispatcher:SetTacticalDisplay( true ) - -- - function AI_A2A_DISPATCHER:SetTacticalDisplay( TacticalDisplay ) - - self.TacticalDisplay = TacticalDisplay - - return self - end - - - --- Set the default damage treshold when defenders will RTB. - -- The default damage treshold is by default set to 40%, which means that when the airplane is 40% damaged, it will go RTB. - -- @param #AI_A2A_DISPATCHER self - -- @param #number DamageThreshold A decimal number between 0 and 1, that expresses the %-tage of the damage treshold before going RTB. - -- @return #AI_A2A_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- - -- -- Now Setup the default damage treshold. - -- A2ADispatcher:SetDefaultDamageThreshold( 0.90 ) -- Go RTB when the airplane 90% damaged. - -- - function AI_A2A_DISPATCHER:SetDefaultDamageThreshold( DamageThreshold ) - - self.DefenderDefault.DamageThreshold = DamageThreshold - - return self - end - - - --- Set the default CAP time interval for squadrons, which will be used to determine a random CAP timing. - -- The default CAP time interval is between 180 and 600 seconds. - -- @param #AI_A2A_DISPATCHER self - -- @param #number CapMinSeconds The minimum amount of seconds for the random time interval. - -- @param #number CapMaxSeconds The maximum amount of seconds for the random time interval. - -- @return #AI_A2A_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- - -- -- Now Setup the default CAP time interval. - -- A2ADispatcher:SetDefaultCapTimeInterval( 300, 1200 ) -- Between 300 and 1200 seconds. - -- - function AI_A2A_DISPATCHER:SetDefaultCapTimeInterval( CapMinSeconds, CapMaxSeconds ) - - self.DefenderDefault.CapMinSeconds = CapMinSeconds - self.DefenderDefault.CapMaxSeconds = CapMaxSeconds - - return self - end - - - --- Set the default CAP limit for squadrons, which will be used to determine how many CAP can be airborne at the same time for the squadron. - -- The default CAP limit is 1 CAP, which means one CAP group being spawned. - -- @param #AI_A2A_DISPATCHER self - -- @param #number CapLimit The maximum amount of CAP that can be airborne at the same time for the squadron. - -- @return #AI_A2A_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- - -- -- Now Setup the default CAP limit. - -- A2ADispatcher:SetDefaultCapLimit( 2 ) -- Maximum 2 CAP per squadron. - -- - function AI_A2A_DISPATCHER:SetDefaultCapLimit( CapLimit ) - - self.DefenderDefault.CapLimit = CapLimit - - return self - end - - - function AI_A2A_DISPATCHER:SetIntercept( InterceptDelay ) - - self.DefenderDefault.InterceptDelay = InterceptDelay - - local Detection = self.Detection -- Functional.Detection#DETECTION_AREAS - Detection:SetIntercept( true, InterceptDelay ) - - return self - end - - - --- Calculates which AI friendlies are nearby the area - -- @param #AI_A2A_DISPATCHER self - -- @param DetectedItem - -- @return #number, Core.CommandCenter#REPORT - function AI_A2A_DISPATCHER:GetAIFriendliesNearBy( DetectedItem ) - - local FriendliesNearBy = self.Detection:GetFriendliesDistance( DetectedItem ) - - return FriendliesNearBy - end - - --- - -- @param #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:GetDefenderTasks() - return self.DefenderTasks or {} - end - - --- - -- @param #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:GetDefenderTask( Defender ) - return self.DefenderTasks[Defender] - end - - --- - -- @param #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:GetDefenderTaskFsm( Defender ) - return self:GetDefenderTask( Defender ).Fsm - end - - --- - -- @param #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:GetDefenderTaskTarget( Defender ) - return self:GetDefenderTask( Defender ).Target - end - - --- - -- @param #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:GetDefenderTaskSquadronName( Defender ) - return self:GetDefenderTask( Defender ).SquadronName - end - - --- - -- @param #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:ClearDefenderTask( Defender ) - if Defender:IsAlive() and self.DefenderTasks[Defender] then - local Target = self.DefenderTasks[Defender].Target - local Message = "Clearing (" .. self.DefenderTasks[Defender].Type .. ") " - Message = Message .. Defender:GetName() - if Target then - Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" - end - self:F( { Target = Message } ) - end - self.DefenderTasks[Defender] = nil - return self - end - - --- - -- @param #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:ClearDefenderTaskTarget( Defender ) - - local DefenderTask = self:GetDefenderTask( Defender ) - - if Defender:IsAlive() and DefenderTask then - local Target = DefenderTask.Target - local Message = "Clearing (" .. DefenderTask.Type .. ") " - Message = Message .. Defender:GetName() - if Target then - Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" - end - self:F( { Target = Message } ) - end - if Defender and DefenderTask and DefenderTask.Target then - DefenderTask.Target = nil - end --- if Defender and DefenderTask then --- if DefenderTask.Fsm:Is( "Fuel" ) --- or DefenderTask.Fsm:Is( "LostControl") --- or DefenderTask.Fsm:Is( "Damaged" ) then --- self:ClearDefenderTask( Defender ) --- end --- end - return self - end - - - --- - -- @param #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:SetDefenderTask( SquadronName, Defender, Type, Fsm, Target ) - - self:F( { SquadronName = SquadronName, Defender = Defender:GetName() } ) - - self.DefenderTasks[Defender] = self.DefenderTasks[Defender] or {} - self.DefenderTasks[Defender].Type = Type - self.DefenderTasks[Defender].Fsm = Fsm - self.DefenderTasks[Defender].SquadronName = SquadronName - - if Target then - self:SetDefenderTaskTarget( Defender, Target ) - end - return self - end - - - --- - -- @param #AI_A2A_DISPATCHER self - -- @param Wrapper.Group#GROUP AIGroup - function AI_A2A_DISPATCHER:SetDefenderTaskTarget( Defender, AttackerDetection ) - - local Message = "(" .. self.DefenderTasks[Defender].Type .. ") " - Message = Message .. Defender:GetName() - Message = Message .. ( AttackerDetection and ( " target " .. AttackerDetection.Index .. " [" .. AttackerDetection.Set:Count() .. "]" ) ) or "" - self:F( { AttackerDetection = Message } ) - if AttackerDetection then - self.DefenderTasks[Defender].Target = AttackerDetection - end - return self - end - - - --- This is the main method to define Squadrons programmatically. - -- Squadrons: - -- - -- * Have a **name or key** that is the identifier or key of the squadron. - -- * Have **specific plane types** defined by **templates**. - -- * Are **located at one specific airbase**. Multiple squadrons can be located at one airbase through. - -- * Optionally have a limited set of **resources**. The default is that squadrons have unlimited resources. - -- - -- The name of the squadron given acts as the **squadron key** in the AI\_A2A\_DISPATCHER:Squadron...() methods. - -- - -- Additionally, squadrons have specific configuration options to: - -- - -- * Control how new aircraft are **taking off** from the airfield (in the air, cold, hot, at the runway). - -- * Control how returning aircraft are **landing** at the airfield (in the air near the airbase, after landing, after engine shutdown). - -- * Control the **grouping** of new aircraft spawned at the airfield. If there is more than one aircraft to be spawned, these may be grouped. - -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of planes and amount of resources, the mission designer can choose to increase or reduce the amount of planes spawned. - -- - -- For performance and bug workaround reasons within DCS, squadrons have different methods to spawn new aircraft or land returning or damaged aircraft. - -- - -- @param #AI_A2A_DISPATCHER self - -- - -- @param #string SquadronName A string (text) that defines the squadron identifier or the key of the Squadron. - -- It can be any name, for example `"104th Squadron"` or `"SQ SQUADRON1"`, whatever. - -- As long as you remember that this name becomes the identifier of your squadron you have defined. - -- You need to use this name in other methods too! - -- - -- @param #string AirbaseName The airbase name where you want to have the squadron located. - -- You need to specify here EXACTLY the name of the airbase as you see it in the mission editor. - -- Examples are `"Batumi"` or `"Tbilisi-Lochini"`. - -- EXACTLY the airbase name, between quotes `""`. - -- To ease the airbase naming when using the LDT editor and IntelliSense, the @{Airbase#AIRBASE} class contains enumerations of the airbases of each map. - -- - -- * Caucasus: @{Airbase#AIRBASE.Caucaus} - -- * Nevada or NTTR: @{Airbase#AIRBASE.Nevada} - -- * Normandy: @{Airbase#AIRBASE.Normandy} - -- - -- @param #string TemplatePrefixes A string or an array of strings specifying the **prefix names of the templates** (not going to explain what is templates here again). - -- Examples are `{ "104th", "105th" }` or `"104th"` or `"Template 1"` or `"BLUE PLANES"`. - -- Just remember that your template (groups late activated) need to start with the prefix you have specified in your code. - -- If you have only one prefix name for a squadron, you don't need to use the `{ }`, otherwise you need to use the brackets. - -- - -- @param #number Resources (optional) A number that specifies how many resources are in stock of the squadron. If not specified, the squadron will have infinite resources available. - -- - -- @usage - -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- - -- @usage - -- -- This will create squadron "Squadron1" at "Batumi" airbase, and will use plane types "SQ1" and has 40 planes in stock... - -- A2ADispatcher:SetSquadron( "Squadron1", "Batumi", "SQ1", 40 ) - -- - -- @usage - -- -- This will create squadron "Sq 1" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" and has 20 planes in stock... - -- -- Note that in this implementation, the A2A dispatcher will select a random plane type when a new plane (group) needs to be spawned for defenses. - -- -- Note the usage of the {} for the airplane templates list. - -- A2ADispatcher:SetSquadron( "Sq 1", "Batumi", { "Mig-29", "Su-27" }, 40 ) - -- - -- @usage - -- -- This will create 2 squadrons "104th" and "23th" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" respectively and each squadron has 10 planes in stock... - -- A2ADispatcher:SetSquadron( "104th", "Batumi", "Mig-29", 10 ) - -- A2ADispatcher:SetSquadron( "23th", "Batumi", "Su-27", 10 ) - -- - -- @usage - -- -- This is an example like the previous, but now with infinite resources. - -- -- The Resources parameter is not given in the SetSquadron method. - -- A2ADispatcher:SetSquadron( "104th", "Batumi", "Mig-29" ) - -- A2ADispatcher:SetSquadron( "23th", "Batumi", "Su-27" ) - -- - -- - -- @return #AI_A2A_DISPATCHER - function AI_A2A_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, Resources ) - - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - - local DefenderSquadron = self.DefenderSquadrons[SquadronName] - - DefenderSquadron.Name = SquadronName - DefenderSquadron.Airbase = AIRBASE:FindByName( AirbaseName ) - if not DefenderSquadron.Airbase then - error( "Cannot find airbase with name:" .. AirbaseName ) - end - - DefenderSquadron.Spawn = {} - if type( TemplatePrefixes ) == "string" then - local SpawnTemplate = TemplatePrefixes - self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 ) - DefenderSquadron.Spawn[1] = self.DefenderSpawns[SpawnTemplate] - else - for TemplateID, SpawnTemplate in pairs( TemplatePrefixes ) do - self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 ) - DefenderSquadron.Spawn[#DefenderSquadron.Spawn+1] = self.DefenderSpawns[SpawnTemplate] - end - end - DefenderSquadron.Resources = Resources - DefenderSquadron.TemplatePrefixes = TemplatePrefixes - - self:E( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, Resources } } ) - - return self - end - - --- Get an item from the Squadron table. - -- @param #AI_A2A_DISPATCHER self - -- @return #table - function AI_A2A_DISPATCHER:GetSquadron( SquadronName ) - local DefenderSquadron = self.DefenderSquadrons[SquadronName] - - if not DefenderSquadron then - error( "Unknown Squadron:" .. SquadronName ) - end - - return DefenderSquadron - end - - - --- Set a CAP for a Squadron. - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Zone#ZONE_BASE} that defines the zone wherein the CAP will be executed. - -- @param #number FloorAltitude The minimum altitude at which the cap can be executed. - -- @param #number CeilingAltitude the maximum altitude at which the cap can be executed. - -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed. - -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed. - -- @param #number EngageMinSpeed The minimum speed at which the engage can be executed. - -- @param #number EngageMaxSpeed The maximum speed at which the engage can be executed. - -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. - -- @return #AI_A2A_DISPATCHER - -- @usage - -- - -- -- CAP Squadron execution. - -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) - -- A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 ) - -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) - -- - -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) - -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) - -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- - -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") - -- A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) - -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- - function AI_A2A_DISPATCHER:SetSquadronCap( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - local Cap = self.DefenderSquadrons[SquadronName].Cap - Cap.Name = SquadronName - Cap.Zone = Zone - Cap.FloorAltitude = FloorAltitude - Cap.CeilingAltitude = CeilingAltitude - Cap.PatrolMinSpeed = PatrolMinSpeed - Cap.PatrolMaxSpeed = PatrolMaxSpeed - Cap.EngageMinSpeed = EngageMinSpeed - Cap.EngageMaxSpeed = EngageMaxSpeed - Cap.AltType = AltType - - self:SetSquadronCapInterval( SquadronName, self.DefenderDefault.CapLimit, self.DefenderDefault.CapMinSeconds, self.DefenderDefault.CapMaxSeconds, 1 ) - - self:E( { CAP = { SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType } } ) - - -- Add the CAP to the EWR network. - - local RecceSet = self.Detection:GetDetectionSetGroup() - RecceSet:FilterPrefixes( DefenderSquadron.TemplatePrefixes ) - RecceSet:FilterStart() - - self.Detection:SetFriendlyPrefixes( DefenderSquadron.TemplatePrefixes ) - - return self - end - - --- Set the squadron CAP parameters. - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @param #number CapLimit (optional) The maximum amount of CAP groups to be spawned. Note that a CAP is a group, so can consist out of 1 to 4 airplanes. The default is 1 CAP group. - -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new CAP will be spawned. The default is 180 seconds. - -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new CAP will be spawned. The default is 600 seconds. - -- @param #number Probability Is not in use, you can skip this parameter. - -- @return #AI_A2A_DISPATCHER - -- @usage - -- - -- -- CAP Squadron execution. - -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) - -- A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 ) - -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) - -- - -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) - -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) - -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- - -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") - -- A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) - -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- - function AI_A2A_DISPATCHER:SetSquadronCapInterval( SquadronName, CapLimit, LowInterval, HighInterval, Probability ) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - local Cap = self.DefenderSquadrons[SquadronName].Cap - if Cap then - Cap.LowInterval = LowInterval or 180 - Cap.HighInterval = HighInterval or 600 - Cap.Probability = Probability or 1 - Cap.CapLimit = CapLimit or 1 - Cap.Scheduler = Cap.Scheduler or SCHEDULER:New( self ) - local Scheduler = Cap.Scheduler -- Core.Scheduler#SCHEDULER - local ScheduleID = Cap.ScheduleID - local Variance = ( Cap.HighInterval - Cap.LowInterval ) / 2 - local Repeat = Cap.LowInterval + Variance - local Randomization = Variance / Repeat - local Start = math.random( 1, Cap.HighInterval ) - - if ScheduleID then - Scheduler:Stop( ScheduleID ) - end - - Cap.ScheduleID = Scheduler:Schedule( self, self.SchedulerCAP, { SquadronName }, Start, Repeat, Randomization ) - else - error( "This squadron does not exist:" .. SquadronName ) - end - - end - - --- - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @return #AI_A2A_DISPATCHER - function AI_A2A_DISPATCHER:GetCAPDelay( SquadronName ) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - local Cap = self.DefenderSquadrons[SquadronName].Cap - if Cap then - return math.random( Cap.LowInterval, Cap.HighInterval ) - else - error( "This squadron does not exist:" .. SquadronName ) - end - end - - --- - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @return #table DefenderSquadron - function AI_A2A_DISPATCHER:CanCAP( SquadronName ) - self:F({SquadronName = SquadronName}) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - if ( not DefenderSquadron.Resources ) or ( DefenderSquadron.Resources and DefenderSquadron.Resources > 0 ) then - - local Cap = DefenderSquadron.Cap - if Cap then - local CapCount = self:CountCapAirborne( SquadronName ) - self:E( { CapCount = CapCount } ) - if CapCount < Cap.CapLimit then - local Probability = math.random() - if Probability <= Cap.Probability then - return DefenderSquadron - end - end - end - end - return nil - end - - - --- - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @return #table DefenderSquadron - function AI_A2A_DISPATCHER:CanGCI( SquadronName ) - self:F({SquadronName = SquadronName}) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - self.DefenderSquadrons[SquadronName].Gci = self.DefenderSquadrons[SquadronName].Gci or {} - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - if ( not DefenderSquadron.Resources ) or ( DefenderSquadron.Resources and DefenderSquadron.Resources > 0 ) then - local Gci = DefenderSquadron.Gci - if Gci then - return DefenderSquadron - end - end - return nil - end - - - --- - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @param #number EngageMinSpeed The minimum speed at which the gci can be executed. - -- @param #number EngageMaxSpeed The maximum speed at which the gci can be executed. - -- @usage - -- - -- -- GCI Squadron execution. - -- A2ADispatcher:SetSquadronGci( "Mozdok", 900, 1200 ) - -- A2ADispatcher:SetSquadronGci( "Novo", 900, 2100 ) - -- A2ADispatcher:SetSquadronGci( "Maykop", 900, 1200 ) - -- - -- @return #AI_A2A_DISPATCHER - function AI_A2A_DISPATCHER:SetSquadronGci( SquadronName, EngageMinSpeed, EngageMaxSpeed ) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - self.DefenderSquadrons[SquadronName].Gci = self.DefenderSquadrons[SquadronName].Gci or {} - - local Intercept = self.DefenderSquadrons[SquadronName].Gci - Intercept.Name = SquadronName - Intercept.EngageMinSpeed = EngageMinSpeed - Intercept.EngageMaxSpeed = EngageMaxSpeed - - self:E( { GCI = { SquadronName, EngageMinSpeed, EngageMaxSpeed } } ) - end - - --- Defines the default amount of extra planes that will take-off as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. - -- The default overhead is 1, so equal balance. The @{#AI_A2A_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, - -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2A missiles payload, may still be less effective than a F-15C with short missiles... - -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. - -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: - -- - -- * Higher than 1, will increase the defense unit amounts. - -- * Lower than 1, will decrease the defense unit amounts. - -- - -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group - -- multiplied by the Overhead and rounded up to the smallest integer. - -- - -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. - -- - -- See example below. - -- - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. - -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. - -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. - -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. - -- - -- A2ADispatcher:SetDefaultOverhead( 1.5 ) - -- - -- @return #AI_A2A_DISPATCHER - function AI_A2A_DISPATCHER:SetDefaultOverhead( Overhead ) - - self.DefenderDefault.Overhead = Overhead - - return self - end - - - --- Defines the amount of extra planes that will take-off as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. - -- The default overhead is 1, so equal balance. The @{#AI_A2A_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, - -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2A missiles payload, may still be less effective than a F-15C with short missiles... - -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. - -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: - -- - -- * Higher than 1, will increase the defense unit amounts. - -- * Lower than 1, will decrease the defense unit amounts. - -- - -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group - -- multiplied by the Overhead and rounded up to the smallest integer. - -- - -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. - -- - -- See example below. - -- - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. - -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. - -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. - -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. - -- - -- A2ADispatcher:SetSquadronOverhead( "SquadronName", 1.5 ) - -- - -- @return #AI_A2A_DISPATCHER - function AI_A2A_DISPATCHER:SetSquadronOverhead( SquadronName, Overhead ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - DefenderSquadron.Overhead = Overhead - - return self - end - - - --- Sets the default grouping of new airplanes spawned. - -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. - -- @param #AI_A2A_DISPATCHER self - -- @param #number Grouping The level of grouping that will be applied of the CAP or GCI defenders. - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Set a grouping by default per 2 airplanes. - -- A2ADispatcher:SetDefaultGrouping( 2 ) - -- - -- - -- @return #AI_A2A_DISPATCHER - function AI_A2A_DISPATCHER:SetDefaultGrouping( Grouping ) - - self.DefenderDefault.Grouping = Grouping - - return self - end - - - --- Sets the grouping of new airplanes spawned. - -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @param #number Grouping The level of grouping that will be applied of the CAP or GCI defenders. - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Set a grouping per 2 airplanes. - -- A2ADispatcher:SetSquadronGrouping( "SquadronName", 2 ) - -- - -- - -- @return #AI_A2A_DISPATCHER - function AI_A2A_DISPATCHER:SetSquadronGrouping( SquadronName, Grouping ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - DefenderSquadron.Grouping = Grouping - - return self - end - - - --- Defines the default method at which new flights will spawn and take-off as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let new flights by default take-off in the air. - -- A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Air ) - -- - -- -- Let new flights by default take-off from the runway. - -- A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Runway ) - -- - -- -- Let new flights by default take-off from the airbase hot. - -- A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Hot ) - -- - -- -- Let new flights by default take-off from the airbase cold. - -- A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Cold ) - -- - -- - -- @return #AI_A2A_DISPATCHER - -- - function AI_A2A_DISPATCHER:SetDefaultTakeoff( Takeoff ) - - self.DefenderDefault.Takeoff = Takeoff - - return self - end - - --- Defines the method at which new flights will spawn and take-off as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let new flights take-off in the air. - -- A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Air ) - -- - -- -- Let new flights take-off from the runway. - -- A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Runway ) - -- - -- -- Let new flights take-off from the airbase hot. - -- A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Hot ) - -- - -- -- Let new flights take-off from the airbase cold. - -- A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Cold ) - -- - -- - -- @return #AI_A2A_DISPATCHER - -- - function AI_A2A_DISPATCHER:SetSquadronTakeoff( SquadronName, Takeoff ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - DefenderSquadron.Takeoff = Takeoff - - return self - end - - - --- Gets the default method at which new flights will spawn and take-off as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let new flights by default take-off in the air. - -- local TakeoffMethod = A2ADispatcher:GetDefaultTakeoff() - -- if TakeOffMethod == , AI_A2A_Dispatcher.Takeoff.InAir then - -- ... - -- end - -- - function AI_A2A_DISPATCHER:GetDefaultTakeoff( ) - - return self.DefenderDefault.Takeoff - end - - --- Gets the method at which new flights will spawn and take-off as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let new flights take-off in the air. - -- local TakeoffMethod = A2ADispatcher:GetSquadronTakeoff( "SquadronName" ) - -- if TakeOffMethod == , AI_A2A_Dispatcher.Takeoff.InAir then - -- ... - -- end - -- - function AI_A2A_DISPATCHER:GetSquadronTakeoff( SquadronName ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - return DefenderSquadron.Takeoff or self.DefenderDefault.Takeoff - end - - - --- Sets flights to default take-off in the air, as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let new flights by default take-off in the air. - -- A2ADispatcher:SetDefaultTakeoffInAir() - -- - -- @return #AI_A2A_DISPATCHER - -- - function AI_A2A_DISPATCHER:SetDefaultTakeoffInAir() - - self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Air ) - - return self - end - - - --- Sets flights to take-off in the air, as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @param #number TakeoffAltitude (optional) The altitude in meters above the ground. If not given, the default takeoff altitude will be used. - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let new flights take-off in the air. - -- A2ADispatcher:SetSquadronTakeoffInAir( "SquadronName" ) - -- - -- @return #AI_A2A_DISPATCHER - -- - function AI_A2A_DISPATCHER:SetSquadronTakeoffInAir( SquadronName, TakeoffAltitude ) - - self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Air ) - - if TakeoffAltitude then - self:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) - end - - return self - end - - - --- Sets flights by default to take-off from the runway, as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let new flights by default take-off from the runway. - -- A2ADispatcher:SetDefaultTakeoffFromRunway() - -- - -- @return #AI_A2A_DISPATCHER - -- - function AI_A2A_DISPATCHER:SetDefaultTakeoffFromRunway() - - self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Runway ) - - return self - end - - - --- Sets flights to take-off from the runway, as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let new flights take-off from the runway. - -- A2ADispatcher:SetSquadronTakeoffFromRunway( "SquadronName" ) - -- - -- @return #AI_A2A_DISPATCHER - -- - function AI_A2A_DISPATCHER:SetSquadronTakeoffFromRunway( SquadronName ) - - self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Runway ) - - return self - end - - - --- Sets flights by default to take-off from the airbase at a hot location, as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let new flights by default take-off at a hot parking spot. - -- A2ADispatcher:SetDefaultTakeoffFromParkingHot() - -- - -- @return #AI_A2A_DISPATCHER - -- - function AI_A2A_DISPATCHER:SetDefaultTakeoffFromParkingHot() - - self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Hot ) - - return self - end - - --- Sets flights to take-off from the airbase at a hot location, as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let new flights take-off in the air. - -- A2ADispatcher:SetSquadronTakeoffFromParkingHot( "SquadronName" ) - -- - -- @return #AI_A2A_DISPATCHER - -- - function AI_A2A_DISPATCHER:SetSquadronTakeoffFromParkingHot( SquadronName ) - - self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Hot ) - - return self - end - - - --- Sets flights to by default take-off from the airbase at a cold location, as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let new flights take-off from a cold parking spot. - -- A2ADispatcher:SetDefaultTakeoffFromParkingCold() - -- - -- @return #AI_A2A_DISPATCHER - -- - function AI_A2A_DISPATCHER:SetDefaultTakeoffFromParkingCold() - - self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Cold ) - - return self - end - - - --- Sets flights to take-off from the airbase at a cold location, as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let new flights take-off from a cold parking spot. - -- A2ADispatcher:SetSquadronTakeoffFromParkingCold( "SquadronName" ) - -- - -- @return #AI_A2A_DISPATCHER - -- - function AI_A2A_DISPATCHER:SetSquadronTakeoffFromParkingCold( SquadronName ) - - self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Cold ) - - return self - end - - - --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected. - -- @param #AI_A2A_DISPATCHER self - -- @param #number TakeoffAltitude The altitude in meters above the ground. - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Set the default takeoff altitude when taking off in the air. - -- A2ADispatcher:SetDefaultTakeoffInAirAltitude( 2000 ) -- This makes planes start at 2000 meters above the ground. - -- - -- @return #AI_A2A_DISPATCHER - -- - function AI_A2A_DISPATCHER:SetDefaultTakeoffInAirAltitude( TakeoffAltitude ) - - self.DefenderDefault.TakeoffAltitude = TakeoffAltitude - - return self - end - - --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected. - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @param #number TakeoffAltitude The altitude in meters above the ground. - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Set the default takeoff altitude when taking off in the air. - -- A2ADispatcher:SetSquadronTakeoffInAirAltitude( "SquadronName", 2000 ) -- This makes planes start at 2000 meters above the ground. - -- - -- @return #AI_A2A_DISPATCHER - -- - function AI_A2A_DISPATCHER:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - DefenderSquadron.TakeoffAltitude = TakeoffAltitude - - return self - end - - - --- Defines the default method at which flights will land and despawn as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let new flights by default despawn near the airbase when returning. - -- A2ADispatcher:SetDefaultLanding( AI_A2A_Dispatcher.Landing.NearAirbase ) - -- - -- -- Let new flights by default despawn after landing land at the runway. - -- A2ADispatcher:SetDefaultLanding( AI_A2A_Dispatcher.Landing.AtRunway ) - -- - -- -- Let new flights by default despawn after landing and parking, and after engine shutdown. - -- A2ADispatcher:SetDefaultLanding( AI_A2A_Dispatcher.Landing.AtEngineShutdown ) - -- - -- @return #AI_A2A_DISPATCHER - function AI_A2A_DISPATCHER:SetDefaultLanding( Landing ) - - self.DefenderDefault.Landing = Landing - - return self - end - - - --- Defines the method at which flights will land and despawn as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let new flights despawn near the airbase when returning. - -- A2ADispatcher:SetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.NearAirbase ) - -- - -- -- Let new flights despawn after landing land at the runway. - -- A2ADispatcher:SetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.AtRunway ) - -- - -- -- Let new flights despawn after landing and parking, and after engine shutdown. - -- A2ADispatcher:SetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.AtEngineShutdown ) - -- - -- @return #AI_A2A_DISPATCHER - function AI_A2A_DISPATCHER:SetSquadronLanding( SquadronName, Landing ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - DefenderSquadron.Landing = Landing - - return self - end - - - --- Gets the default method at which flights will land and despawn as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let new flights by default despawn near the airbase when returning. - -- local LandingMethod = A2ADispatcher:GetDefaultLanding( AI_A2A_Dispatcher.Landing.NearAirbase ) - -- if LandingMethod == AI_A2A_Dispatcher.Landing.NearAirbase then - -- ... - -- end - -- - function AI_A2A_DISPATCHER:GetDefaultLanding() - - return self.DefenderDefault.Landing - end - - - --- Gets the method at which flights will land and despawn as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let new flights despawn near the airbase when returning. - -- local LandingMethod = A2ADispatcher:GetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.NearAirbase ) - -- if LandingMethod == AI_A2A_Dispatcher.Landing.NearAirbase then - -- ... - -- end - -- - function AI_A2A_DISPATCHER:GetSquadronLanding( SquadronName ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - return DefenderSquadron.Landing or self.DefenderDefault.Landing - end - - - --- Sets flights by default to land and despawn near the airbase in the air, as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let flights by default to land near the airbase and despawn. - -- A2ADispatcher:SetDefaultLandingNearAirbase() - -- - -- @return #AI_A2A_DISPATCHER - function AI_A2A_DISPATCHER:SetDefaultLandingNearAirbase() - - self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.NearAirbase ) - - return self - end - - - --- Sets flights to land and despawn near the airbase in the air, as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let flights to land near the airbase and despawn. - -- A2ADispatcher:SetSquadronLandingNearAirbase( "SquadronName" ) - -- - -- @return #AI_A2A_DISPATCHER - function AI_A2A_DISPATCHER:SetSquadronLandingNearAirbase( SquadronName ) - - self:SetSquadronLanding( SquadronName, AI_A2A_DISPATCHER.Landing.NearAirbase ) - - return self - end - - - --- Sets flights by default to land and despawn at the runway, as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let flights by default land at the runway and despawn. - -- A2ADispatcher:SetDefaultLandingAtRunway() - -- - -- @return #AI_A2A_DISPATCHER - function AI_A2A_DISPATCHER:SetDefaultLandingAtRunway() - - self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.AtRunway ) - - return self - end - - - --- Sets flights to land and despawn at the runway, as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let flights land at the runway and despawn. - -- A2ADispatcher:SetSquadronLandingAtRunway( "SquadronName" ) - -- - -- @return #AI_A2A_DISPATCHER - function AI_A2A_DISPATCHER:SetSquadronLandingAtRunway( SquadronName ) - - self:SetSquadronLanding( SquadronName, AI_A2A_DISPATCHER.Landing.AtRunway ) - - return self - end - - - --- Sets flights by default to land and despawn at engine shutdown, as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let flights by default land and despawn at engine shutdown. - -- A2ADispatcher:SetDefaultLandingAtEngineShutdown() - -- - -- @return #AI_A2A_DISPATCHER - function AI_A2A_DISPATCHER:SetDefaultLandingAtEngineShutdown() - - self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.AtEngineShutdown ) - - return self - end - - - --- Sets flights to land and despawn at engine shutdown, as part of the defense system. - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @usage: - -- - -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- - -- -- Let flights land and despawn at engine shutdown. - -- A2ADispatcher:SetSquadronLandingAtEngineShutdown( "SquadronName" ) - -- - -- @return #AI_A2A_DISPATCHER - function AI_A2A_DISPATCHER:SetSquadronLandingAtEngineShutdown( SquadronName ) - - self:SetSquadronLanding( SquadronName, AI_A2A_DISPATCHER.Landing.AtEngineShutdown ) - - return self - end - - --- Set the default fuel treshold when defenders will RTB or Refuel in the air. - -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. - -- @param #AI_A2A_DISPATCHER self - -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. - -- @return #AI_A2A_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- - -- -- Now Setup the default fuel treshold. - -- A2ADispatcher:SetDefaultRefuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. - -- - function AI_A2A_DISPATCHER:SetDefaultFuelThreshold( FuelThreshold ) - - self.DefenderDefault.FuelThreshold = FuelThreshold - - return self - end - - - --- Set the fuel treshold for the squadron when defenders will RTB or Refuel in the air. - -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. - -- @return #AI_A2A_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- - -- -- Now Setup the default fuel treshold. - -- A2ADispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. - -- - function AI_A2A_DISPATCHER:SetSquadronFuelThreshold( SquadronName, FuelThreshold ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - DefenderSquadron.FuelThreshold = FuelThreshold - - return self - end - - --- Set the default tanker where defenders will Refuel in the air. - -- @param #AI_A2A_DISPATCHER self - -- @param #strig TankerName A string defining the group name of the Tanker as defined within the Mission Editor. - -- @return #AI_A2A_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- - -- -- Now Setup the default fuel treshold. - -- A2ADispatcher:SetDefaultRefuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. - -- - -- -- Now Setup the default tanker. - -- A2ADispatcher:SetDefaultTanker( "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. - function AI_A2A_DISPATCHER:SetDefaultTanker( TankerName ) - - self.DefenderDefault.TankerName = TankerName - - return self - end - - - --- Set the squadron tanker where defenders will Refuel in the air. - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @param #strig TankerName A string defining the group name of the Tanker as defined within the Mission Editor. - -- @return #AI_A2A_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- - -- -- Now Setup the squadron fuel treshold. - -- A2ADispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. - -- - -- -- Now Setup the squadron tanker. - -- A2ADispatcher:SetSquadronTanker( "SquadronName", "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. - function AI_A2A_DISPATCHER:SetSquadronTanker( SquadronName, TankerName ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - DefenderSquadron.TankerName = TankerName - - return self - end - - - - - --- @param #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:AddDefenderToSquadron( Squadron, Defender, Size ) - self.Defenders = self.Defenders or {} - local DefenderName = Defender:GetName() - self.Defenders[ DefenderName ] = Squadron - if Squadron.Resources then - Squadron.Resources = Squadron.Resources - Size - end - self:E( { DefenderName = DefenderName, SquadronResources = Squadron.Resources } ) - end - - --- @param #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:RemoveDefenderFromSquadron( Squadron, Defender ) - self.Defenders = self.Defenders or {} - local DefenderName = Defender:GetName() - if Squadron.Resources then - Squadron.Resources = Squadron.Resources + Defender:GetSize() - end - self.Defenders[ DefenderName ] = nil - self:F( { DefenderName = DefenderName, SquadronResources = Squadron.Resources } ) - end - - function AI_A2A_DISPATCHER:GetSquadronFromDefender( Defender ) - self.Defenders = self.Defenders or {} - local DefenderName = Defender:GetName() - self:F( { DefenderName = DefenderName } ) - return self.Defenders[ DefenderName ] - end - - - --- Creates an SWEEP task when there are targets for it. - -- @param #AI_A2A_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem - -- @return Set#SET_UNIT TargetSetUnit: The target set of units. - -- @return #nil If there are no targets to be set. - function AI_A2A_DISPATCHER:EvaluateSWEEP( DetectedItem ) - self:F( { DetectedItem.ItemID } ) - - local DetectedSet = DetectedItem.Set - local DetectedZone = DetectedItem.Zone - - - if DetectedItem.IsDetected == false then - - -- Here we're doing something advanced... We're copying the DetectedSet. - local TargetSetUnit = SET_UNIT:New() - TargetSetUnit:SetDatabase( DetectedSet ) - TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - - return TargetSetUnit - end - - return nil - end - - --- - -- @param #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:CountCapAirborne( SquadronName ) - - local CapCount = 0 - - local DefenderSquadron = self.DefenderSquadrons[SquadronName] - if DefenderSquadron then - for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do - if DefenderTask.SquadronName == SquadronName then - if DefenderTask.Type == "CAP" then - if AIGroup:IsAlive() then - -- Check if the CAP is patrolling or engaging. If not, this is not a valid CAP, even if it is alive! - -- The CAP could be damaged, lost control, or out of fuel! - if DefenderTask.Fsm:Is( "Patrolling" ) or DefenderTask.Fsm:Is( "Engaging" ) or DefenderTask.Fsm:Is( "Refuelling" )then - CapCount = CapCount + 1 - end - end - end - end - end - end - - return CapCount - end - - - --- - -- @param #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:CountDefendersEngaged( AttackerDetection ) - - -- First, count the active AIGroups Units, targetting the DetectedSet - local DefenderCount = 0 - - self:E( "Counting Defenders Engaged for Attacker:" ) - local DetectedSet = AttackerDetection.Set - DetectedSet:Flush() - - local DefenderTasks = self:GetDefenderTasks() - for DefenderGroup, DefenderTask in pairs( DefenderTasks ) do - local Defender = DefenderGroup -- Wrapper.Group#GROUP - local DefenderTaskTarget = DefenderTask.Target - local DefenderSquadronName = DefenderTask.SquadronName - - if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then - local Squadron = self:GetSquadron( DefenderSquadronName ) - local SquadronOverhead = Squadron.Overhead or self.DefenderDefault.Overhead - - local DefenderSize = Defender:GetInitialSize() - DefenderCount = DefenderCount + DefenderSize / SquadronOverhead - self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) - end - end - - self:F( { DefenderCount = DefenderCount } ) - - return DefenderCount - end - - --- - -- @param #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:CountDefendersToBeEngaged( AttackerDetection, DefenderCount ) - - local Friendlies = nil - - local AttackerSet = AttackerDetection.Set - local AttackerCount = AttackerSet:Count() - - local DefenderFriendlies = self:GetAIFriendliesNearBy( AttackerDetection ) - - for FriendlyDistance, AIFriendly in UTILS.spairs( DefenderFriendlies or {} ) do - -- We only allow to ENGAGE targets as long as the Units on both sides are balanced. - if AttackerCount > DefenderCount then - local Friendly = AIFriendly:GetGroup() -- Wrapper.Group#GROUP - if Friendly and Friendly:IsAlive() then - -- Ok, so we have a friendly near the potential target. - -- Now we need to check if the AIGroup has a Task. - local DefenderTask = self:GetDefenderTask( Friendly ) - if DefenderTask then - -- The Task should be CAP or GCI - if DefenderTask.Type == "CAP" or DefenderTask.Type == "GCI" then - -- If there is no target, then add the AIGroup to the ResultAIGroups for Engagement to the AttackerSet - if DefenderTask.Target == nil then - if DefenderTask.Fsm:Is( "Returning" ) - or DefenderTask.Fsm:Is( "Patrolling" ) then - Friendlies = Friendlies or {} - Friendlies[Friendly] = Friendly - DefenderCount = DefenderCount + Friendly:GetSize() - self:F( { Friendly = Friendly:GetName(), FriendlyDistance = FriendlyDistance } ) - end - end - end - end - end - else - break - end - end - - return Friendlies - end - - - - --- - -- @param #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:onafterCAP( From, Event, To, SquadronName ) - - self:F({SquadronName = SquadronName}) - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} - - local DefenderSquadron = self:CanCAP( SquadronName ) - - if DefenderSquadron then - - local Cap = DefenderSquadron.Cap - - if Cap then - - local Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Functional.Spawn#SPAWN - local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping - Spawn:InitGrouping( DefenderGrouping ) - - local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) - local DefenderCAP = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, TakeoffMethod, DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude ) - self:AddDefenderToSquadron( DefenderSquadron, DefenderCAP, DefenderGrouping ) - - if DefenderCAP then - - local Fsm = AI_A2A_CAP:New( DefenderCAP, Cap.Zone, Cap.FloorAltitude, Cap.CeilingAltitude, Cap.PatrolMinSpeed, Cap.PatrolMaxSpeed, Cap.EngageMinSpeed, Cap.EngageMaxSpeed, Cap.AltType ) - Fsm:SetDispatcher( self ) - Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) - Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) - Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) - Fsm:SetDisengageRadius( self.DisengageRadius ) - Fsm:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName ) - Fsm:Start() - - self:SetDefenderTask( SquadronName, DefenderCAP, "CAP", Fsm ) - - function Fsm:onafterTakeoff( Defender, From, Event, To ) - self:F({"GCI Birth", Defender:GetName()}) - --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - - local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - - if Squadron then - Fsm:__Patrol( 2 ) -- Start Patrolling - end - end - - function Fsm:onafterRTB( Defender, From, Event, To ) - self:F({"CAP RTB", Defender:GetName()}) - self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) - local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER - Dispatcher:ClearDefenderTaskTarget( Defender ) - end - - --- @param #AI_A2A_DISPATCHER self - function Fsm:onafterHome( Defender, From, Event, To, Action ) - self:E({"CAP Home", Defender:GetName()}) - self:GetParent(self).onafterHome( self, Defender, From, Event, To ) - - local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - - if Action and Action == "Destroy" then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - end - - if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2A_DISPATCHER.Landing.NearAirbase then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - end - end - - end - end - end - - end - - - --- - -- @param #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:onafterENGAGE( From, Event, To, AttackerDetection, Defenders ) - - if Defenders then - - for DefenderID, Defender in pairs( Defenders ) do - - local Fsm = self:GetDefenderTaskFsm( Defender ) - Fsm:__Engage( 1, AttackerDetection.Set ) -- Engage on the TargetSetUnit - - self:SetDefenderTaskTarget( Defender, AttackerDetection ) - - end - end - end - - --- - -- @param #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:onafterGCI( From, Event, To, AttackerDetection, DefendersMissing, DefenderFriendlies ) - - self:F( { From, Event, To, AttackerDetection.Index, DefendersMissing, DefenderFriendlies } ) - - local AttackerSet = AttackerDetection.Set - local AttackerUnit = AttackerSet:GetFirst() - - if AttackerUnit and AttackerUnit:IsAlive() then - local AttackerCount = AttackerSet:Count() - local DefenderCount = 0 - - for DefenderID, DefenderGroup in pairs( DefenderFriendlies or {} ) do - - local Fsm = self:GetDefenderTaskFsm( DefenderGroup ) - Fsm:__Engage( 1, AttackerSet ) -- Engage on the TargetSetUnit - - self:SetDefenderTaskTarget( DefenderGroup, AttackerDetection ) - - DefenderCount = DefenderCount + DefenderGroup:GetSize() - end - - self:F( { DefenderCount = DefenderCount, DefendersMissing = DefendersMissing } ) - DefenderCount = DefendersMissing - - local ClosestDistance = 0 - local ClosestDefenderSquadronName = nil - - local BreakLoop = false - - while( DefenderCount > 0 and not BreakLoop ) do - - self:F( { DefenderSquadrons = self.DefenderSquadrons } ) - - for SquadronName, DefenderSquadron in pairs( self.DefenderSquadrons or {} ) do - - self:F( { GCI = DefenderSquadron.Gci } ) - - for InterceptID, Intercept in pairs( DefenderSquadron.Gci or {} ) do - - self:F( { DefenderSquadron } ) - local SpawnCoord = DefenderSquadron.Airbase:GetCoordinate() -- Core.Point#COORDINATE - local AttackerCoord = AttackerUnit:GetCoordinate() - local InterceptCoord = AttackerDetection.InterceptCoord - self:F( { InterceptCoord = InterceptCoord } ) - if InterceptCoord then - local InterceptDistance = SpawnCoord:Get2DDistance( InterceptCoord ) - local AirbaseDistance = SpawnCoord:Get2DDistance( AttackerCoord ) - self:F( { InterceptDistance = InterceptDistance, AirbaseDistance = AirbaseDistance, InterceptCoord = InterceptCoord } ) - - if ClosestDistance == 0 or InterceptDistance < ClosestDistance then - - -- Only intercept if the distance to target is smaller or equal to the GciRadius limit. - if AirbaseDistance <= self.GciRadius then - ClosestDistance = InterceptDistance - ClosestDefenderSquadronName = SquadronName - end - end - end - end - end - - if ClosestDefenderSquadronName then - - local DefenderSquadron = self:CanGCI( ClosestDefenderSquadronName ) - - if DefenderSquadron then - - local Gci = self.DefenderSquadrons[ClosestDefenderSquadronName].Gci - - if Gci then - - local DefenderOverhead = DefenderSquadron.Overhead or self.DefenderDefault.Overhead - local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping - local DefendersNeeded = math.ceil( DefenderCount * DefenderOverhead ) - - self:F( { Overhead = DefenderOverhead, SquadronOverhead = DefenderSquadron.Overhead , DefaultOverhead = self.DefenderDefault.Overhead } ) - self:F( { Grouping = DefenderGrouping, SquadronGrouping = DefenderSquadron.Grouping, DefaultGrouping = self.DefenderDefault.Grouping } ) - self:F( { DefendersCount = DefenderCount, DefendersNeeded = DefendersNeeded } ) - - -- DefenderSquadron.Resources can have the value nil, which expresses unlimited resources. - -- DefendersNeeded cannot exceed DefenderSquadron.Resources! - if DefenderSquadron.Resources and DefendersNeeded > DefenderSquadron.Resources then - DefendersNeeded = DefenderSquadron.Resources - BreakLoop = true - end - - while ( DefendersNeeded > 0 ) do - - local Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Functional.Spawn#SPAWN - local DefenderGrouping = ( DefenderGrouping < DefendersNeeded ) and DefenderGrouping or DefendersNeeded - if DefenderGrouping then - Spawn:InitGrouping( DefenderGrouping ) - else - Spawn:InitGrouping() - end - - local TakeoffMethod = self:GetSquadronTakeoff( ClosestDefenderSquadronName ) - local DefenderGCI = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, TakeoffMethod, DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude ) -- Wrapper.Group#GROUP - self:E( { GCIDefender = DefenderGCI:GetName() } ) - - DefendersNeeded = DefendersNeeded - DefenderGrouping - - self:AddDefenderToSquadron( DefenderSquadron, DefenderGCI, DefenderGrouping ) - - if DefenderGCI then - - DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead - - local Fsm = AI_A2A_GCI:New( DefenderGCI, Gci.EngageMinSpeed, Gci.EngageMaxSpeed ) - Fsm:SetDispatcher( self ) - Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) - Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) - Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) - Fsm:SetDisengageRadius( self.DisengageRadius ) - Fsm:Start() - - - self:SetDefenderTask( ClosestDefenderSquadronName, DefenderGCI, "GCI", Fsm, AttackerDetection ) - - - function Fsm:onafterTakeoff( Defender, From, Event, To ) - self:F({"GCI Birth", Defender:GetName()}) - --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - - local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - local DefenderTarget = Dispatcher:GetDefenderTaskTarget( Defender ) - - if DefenderTarget then - Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit - end - end - - function Fsm:onafterRTB( Defender, From, Event, To ) - self:F({"GCI RTB", Defender:GetName()}) - self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) - - local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER - Dispatcher:ClearDefenderTaskTarget( Defender ) - end - - --- @param #AI_A2A_DISPATCHER self - function Fsm:onafterLostControl( Defender, From, Event, To ) - self:F({"GCI LostControl", Defender:GetName()}) - self:GetParent(self).onafterHome( self, Defender, From, Event, To ) - - local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - if Defender:IsAboveRunway() then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - end - end - - --- @param #AI_A2A_DISPATCHER self - function Fsm:onafterHome( Defender, From, Event, To, Action ) - self:F({"GCI Home", Defender:GetName()}) - self:GetParent(self).onafterHome( self, Defender, From, Event, To ) - - local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - - if Action and Action == "Destroy" then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - end - - if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2A_DISPATCHER.Landing.NearAirbase then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - end - end - end -- if DefenderGCI then - end -- while ( DefendersNeeded > 0 ) do - end - else - -- No more resources, try something else. - -- Subject for a later enhancement to try to depart from another squadron and disable this one. - BreakLoop = true - break - end - else - -- There isn't any closest airbase anymore, break the loop. - break - end - end -- if DefenderSquadron then - end -- if AttackerUnit - end - - - - --- Creates an ENGAGE task when there are human friendlies airborne near the targets. - -- @param #AI_A2A_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem - -- @return Set#SET_UNIT TargetSetUnit: The target set of units. - -- @return #nil If there are no targets to be set. - function AI_A2A_DISPATCHER:EvaluateENGAGE( DetectedItem ) - self:F( { DetectedItem.ItemID } ) - - -- First, count the active AIGroups Units, targetting the DetectedSet - local DefenderCount = self:CountDefendersEngaged( DetectedItem ) - local DefenderGroups = self:CountDefendersToBeEngaged( DetectedItem, DefenderCount ) - - self:F( { DefenderCount = DefenderCount } ) - - -- Only allow ENGAGE when: - -- 1. There are friendly units near the detected attackers. - -- 2. There is sufficient fuel - -- 3. There is sufficient ammo - -- 4. The plane is not damaged - if DefenderGroups and DetectedItem.IsDetected == true then - - return DefenderGroups - end - - return nil, nil - end - - --- Creates an GCI task when there are targets for it. - -- @param #AI_A2A_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem - -- @return Set#SET_UNIT TargetSetUnit: The target set of units. - -- @return #nil If there are no targets to be set. - function AI_A2A_DISPATCHER:EvaluateGCI( DetectedItem ) - self:F( { DetectedItem.ItemID } ) - - local AttackerSet = DetectedItem.Set - local AttackerCount = AttackerSet:Count() - - -- First, count the active AIGroups Units, targetting the DetectedSet - local DefenderCount = self:CountDefendersEngaged( DetectedItem ) - local DefendersMissing = AttackerCount - DefenderCount - self:F( { AttackerCount = AttackerCount, DefenderCount = DefenderCount, DefendersMissing = DefendersMissing } ) - - local Friendlies = self:CountDefendersToBeEngaged( DetectedItem, DefenderCount ) - - if DetectedItem.IsDetected == true then - - return DefendersMissing, Friendlies - end - - return nil, nil - end - - - --- Assigns A2A AI Tasks in relation to the detected items. - -- @param #AI_A2A_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Detection#DETECTION_BASE} derived object. - -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. - function AI_A2A_DISPATCHER:ProcessDetected( Detection ) - - local AreaMsg = {} - local TaskMsg = {} - local ChangeMsg = {} - - local TaskReport = REPORT:New() - - - for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do - local AIGroup = AIGroup -- Wrapper.Group#GROUP - if not AIGroup:IsAlive() then - local DefenderTaskFsm = self:GetDefenderTaskFsm( AIGroup ) - self:E( { Defender = AIGroup:GetName(), DefenderState = DefenderTaskFsm:GetState() } ) - if not DefenderTaskFsm:Is( "Started" ) then - self:ClearDefenderTask( AIGroup ) - end - else - if DefenderTask.Target then - local AttackerItem = Detection:GetDetectedItem( DefenderTask.Target.Index ) - if not AttackerItem then - self:F( { "Removing obsolete Target:", DefenderTask.Target.Index } ) - self:ClearDefenderTaskTarget( AIGroup ) - else - if DefenderTask.Target.Set then - local AttackerCount = DefenderTask.Target.Set:Count() - if AttackerCount == 0 then - self:F( { "All Targets destroyed in Target, removing:", DefenderTask.Target.Index } ) - self:ClearDefenderTaskTarget( AIGroup ) - end - end - end - end - end - end - - local Report = REPORT:New( "\nTactical Overview" ) - - -- Now that all obsolete tasks are removed, loop through the detected targets. - for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do - - local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem - local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT - local DetectedCount = DetectedSet:Count() - local DetectedZone = DetectedItem.Zone - - self:F( { "Target ID", DetectedItem.ItemID } ) - DetectedSet:Flush() - - local DetectedID = DetectedItem.ID - local DetectionIndex = DetectedItem.Index - local DetectedItemChanged = DetectedItem.Changed - - do - local Friendlies = self:EvaluateENGAGE( DetectedItem ) -- Returns a SetUnit if there are targets to be GCIed... - if Friendlies then - self:F( { AIGroups = Friendlies } ) - self:ENGAGE( DetectedItem, Friendlies ) - end - end - - do - local DefendersMissing, Friendlies = self:EvaluateGCI( DetectedItem ) - if DefendersMissing then - self:F( { DefendersMissing = DefendersMissing } ) - self:GCI( DetectedItem, DefendersMissing, Friendlies ) - end - end - - if self.TacticalDisplay then - -- Show tactical situation - Report:Add( string.format( "\n - Target %s ( %s ): ( #%d ) %s" , DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Set:GetObjectNames() ) ) - for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do - local Defender = Defender -- Wrapper.Group#GROUP - if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then - local Fuel = Defender:GetFuel() * 100 - local Damage = Defender:GetLife() / Defender:GetLife0() * 100 - Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", - Defender:GetName(), - DefenderTask.Type, - DefenderTask.Fsm:GetState(), - Defender:GetSize(), - Fuel, - Damage, - Defender:HasTask() == true and "Executing" or "Idle" ) ) - end - end - end - end - - if self.TacticalDisplay then - Report:Add( "\n - No Targets:") - local TaskCount = 0 - for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do - TaskCount = TaskCount + 1 - local Defender = Defender -- Wrapper.Group#GROUP - if not DefenderTask.Target then - local DefenderHasTask = Defender:HasTask() - local Fuel = Defender:GetFuel() * 100 - local Damage = Defender:GetLife() / Defender:GetLife0() * 100 - Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", - Defender:GetName(), - DefenderTask.Type, - DefenderTask.Fsm:GetState(), - Defender:GetSize(), - Fuel, - Damage, - Defender:HasTask() == true and "Executing" or "Idle" ) ) - end - end - Report:Add( string.format( "\n - %d Tasks", TaskCount ) ) - - self:E( Report:Text( "\n" ) ) - trigger.action.outText( Report:Text( "\n" ), 25 ) - end - - return true - end - -end - -do - - --- Calculates which HUMAN friendlies are nearby the area - -- @param #AI_A2A_DISPATCHER self - -- @param DetectedItem - -- @return #number, Core.CommandCenter#REPORT - function AI_A2A_DISPATCHER:GetPlayerFriendliesNearBy( DetectedItem ) - - local DetectedSet = DetectedItem.Set - local PlayersNearBy = self.Detection:GetPlayersNearBy( DetectedItem ) - - local PlayerTypes = {} - local PlayersCount = 0 - - if PlayersNearBy then - local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() - for PlayerUnitName, PlayerUnitData in pairs( PlayersNearBy ) do - local PlayerUnit = PlayerUnitData -- Wrapper.Unit#UNIT - local PlayerName = PlayerUnit:GetPlayerName() - --self:E( { PlayerName = PlayerName, PlayerUnit = PlayerUnit } ) - if PlayerUnit:IsAirPlane() and PlayerName ~= nil then - local FriendlyUnitThreatLevel = PlayerUnit:GetThreatLevel() - PlayersCount = PlayersCount + 1 - local PlayerType = PlayerUnit:GetTypeName() - PlayerTypes[PlayerName] = PlayerType - if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then - end - end - end - - end - - --self:E( { PlayersCount = PlayersCount } ) - - local PlayerTypesReport = REPORT:New() - - if PlayersCount > 0 then - for PlayerName, PlayerType in pairs( PlayerTypes ) do - PlayerTypesReport:Add( string.format('"%s" in %s', PlayerName, PlayerType ) ) - end - else - PlayerTypesReport:Add( "-" ) - end - - - return PlayersCount, PlayerTypesReport - end - - --- Calculates which friendlies are nearby the area - -- @param #AI_A2A_DISPATCHER self - -- @param DetectedItem - -- @return #number, Core.CommandCenter#REPORT - function AI_A2A_DISPATCHER:GetFriendliesNearBy( Target ) - - local DetectedSet = Target.Set - local FriendlyUnitsNearBy = self.Detection:GetFriendliesNearBy( Target ) - - local FriendlyTypes = {} - local FriendliesCount = 0 - - if FriendlyUnitsNearBy then - local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() - for FriendlyUnitName, FriendlyUnitData in pairs( FriendlyUnitsNearBy ) do - local FriendlyUnit = FriendlyUnitData -- Wrapper.Unit#UNIT - if FriendlyUnit:IsAirPlane() then - local FriendlyUnitThreatLevel = FriendlyUnit:GetThreatLevel() - FriendliesCount = FriendliesCount + 1 - local FriendlyType = FriendlyUnit:GetTypeName() - FriendlyTypes[FriendlyType] = FriendlyTypes[FriendlyType] and ( FriendlyTypes[FriendlyType] + 1 ) or 1 - if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then - end - end - end - - end - - --self:E( { FriendliesCount = FriendliesCount } ) - - local FriendlyTypesReport = REPORT:New() - - if FriendliesCount > 0 then - for FriendlyType, FriendlyTypeCount in pairs( FriendlyTypes ) do - FriendlyTypesReport:Add( string.format("%d of %s", FriendlyTypeCount, FriendlyType ) ) - end - else - FriendlyTypesReport:Add( "-" ) - end - - - return FriendliesCount, FriendlyTypesReport - end - - --- - -- @param AI_A2A_DISPATCHER - -- @param #string SquadronName The squadron name. - function AI_A2A_DISPATCHER:SchedulerCAP( SquadronName ) - self:CAP( SquadronName ) - end - -end - -do - - --- @type AI_A2A_GCICAP - -- @extends #AI_A2A_DISPATCHER - - --- # AI\_A2A\_GCICAP class, extends @{AI_A2A_Dispatcher#AI_A2A_DISPATCHER} - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia1.JPG) - -- - -- The AI_A2A_GCICAP class is designed to create an automatic air defence system for a coalition setting up GCI and CAP air defenses. - -- The class derives from @{AI#AI_A2A_DISPATCHER} and thus, all the methods that are defined in the @{AI#AI_A2A_DISPATCHER} class, can be used also in AI\_A2A\_GCICAP. - -- - -- ==== - -- - -- # Demo Missions - -- - -- ### [AI\_A2A\_GCICAP for Caucasus](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-200%20-%20AI_A2A%20-%20GCICAP%20Demonstration) - -- ### [AI\_A2A\_GCICAP for NTTR](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-210%20-%20NTTR%20AI_A2A_GCICAP%20Demonstration) - -- ### [AI\_A2A\_GCICAP for Normandy](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-220%20-%20NORMANDY%20AI_A2A_GCICAP%20Demonstration) - -- - -- ### [AI\_A2A\_GCICAP for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching) - -- - -- ==== - -- - -- # YouTube Channel - -- - -- ### [DCS WORLD - MOOSE - A2A GCICAP - Build an automatic A2A Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) - -- - -- === - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia3.JPG) - -- - -- AI\_A2A\_GCICAP includes automatic spawning of Combat Air Patrol aircraft (CAP) and Ground Controlled Intercept aircraft (GCI) in response to enemy - -- air movements that are detected by an airborne or ground based radar network. - -- - -- With a little time and with a little work it provides the mission designer with a convincing and completely automatic air defence system. - -- - -- The AI_A2A_GCICAP provides a lightweight configuration method using the mission editor. Within a very short time, and with very little coding, - -- the mission designer is able to configure a complete A2A defense system for a coalition using the DCS Mission Editor available functions. - -- Using the DCS Mission Editor, you define borders of the coalition which are guarded by GCICAP, - -- configure airbases to belong to the coalition, define squadrons flying certain types of planes or payloads per airbase, and define CAP zones. - -- **Very little lua needs to be applied, a one liner**, which is fully explained below, which can be embedded - -- right in a DO SCRIPT trigger action or in a larger DO SCRIPT FILE trigger action. - -- - -- CAP flights will take off and proceed to designated CAP zones where they will remain on station until the ground radars direct them to intercept - -- detected enemy aircraft or they run short of fuel and must return to base (RTB). - -- - -- When a CAP flight leaves their zone to perform a GCI or return to base a new CAP flight will spawn to take its place. - -- If all CAP flights are engaged or RTB then additional GCI interceptors will scramble to intercept unengaged enemy aircraft under ground radar control. - -- - -- In short it is a plug in very flexible and configurable air defence module for DCS World. - -- - -- ==== - -- - -- # The following actions need to be followed when using AI\_A2A\_GCICAP in your mission: - -- - -- ## 1) Configure a working AI\_A2A\_GCICAP defense system for ONE coalition. - -- - -- ### 1.1) Define which airbases are for which coalition. - -- - -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_1.JPG) - -- - -- Color the airbases red or blue. You can do this by selecting the airbase on the map, and select the coalition blue or red. - -- - -- ### 1.2) Place groups of units given a name starting with a **EWR prefix** of your choice to build your EWR network. - -- - -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_2.JPG) - -- - -- **All EWR groups starting with the EWR prefix (text) will be included in the detection system.** - -- - -- An EWR network, or, Early Warning Radar network, is used to early detect potential airborne targets and to understand the position of patrolling targets of the enemy. - -- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. - -- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US). - -- Additionally, ANY other radar capable unit can be part of the EWR network! - -- Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar. - -- The position of these units is very important as they need to provide enough coverage - -- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them. - -- - -- Additionally in a hot war situation where the border is no longer respected the placement of radars has a big effect on how fast the war escalates. - -- For example if they are a long way forward and can detect enemy planes on the ground and taking off - -- they will start to vector CAP and GCI flights to attack them straight away which will immediately draw a response from the other coalition. - -- Having the radars further back will mean a slower escalation because fewer targets will be detected and - -- therefore less CAP and GCI flights will spawn and this will tend to make just the border area active rather than a melee over the whole map. - -- It all depends on what the desired effect is. - -- - -- EWR networks are **dynamically maintained**. By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network, - -- increasing or decreasing the radar coverage of the Early Warning System. - -- - -- ### 1.3) Place Airplane or Helicopter Groups with late activation switched on above the airbases to define Squadrons. - -- - -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_3.JPG) - -- - -- These are **templates**, with a given name starting with a **Template prefix** above each airbase that you wanna have a squadron. - -- These **templates** need to be within 1.5km from the airbase center. They don't need to have a slot at the airplane, they can just be positioned above the airbase, - -- without a route, and should only have ONE unit. - -- - -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_4.JPG) - -- - -- **All airplane or helicopter groups that are starting with any of the choosen Template Prefixes will result in a squadron created at the airbase.** - -- - -- ### 1.4) Place floating helicopters to create the CAP zones defined by its route points. - -- - -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_5.JPG) - -- - -- **All airplane or helicopter groups that are starting with any of the choosen Template Prefixes will result in a squadron created at the airbase.** - -- - -- The helicopter indicates the start of the CAP zone. - -- The route points define the form of the CAP zone polygon. - -- - -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_6.JPG) - -- - -- **The place of the helicopter is important, as the airbase closest to the helicopter will be the airbase from where the CAP planes will take off for CAP.** - -- - -- ## 2) There are a lot of defaults set, which can be further modified using the methods in @{AI#AI_A2A_DISPATCHER}: - -- - -- ### 2.1) Planes are taking off in the air from the airbases. - -- - -- This prevents airbases to get cluttered with airplanes taking off, it also reduces the risk of human players colliding with taxiiing airplanes, - -- resulting in the airbase to halt operations. - -- - -- You can change the way how planes take off by using the inherited methods from AI\_A2A\_DISPATCHER: - -- - -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoff}() is the generic configuration method to control takeoff from the air, hot, cold or from the runway. See the method for further details. - -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAir}() will spawn new aircraft from the squadron directly in the air. - -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingCold}() will spawn new aircraft in without running engines at a parking spot at the airfield. - -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield. - -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromRunway}() will spawn new aircraft at the runway at the airfield. - -- - -- Use these methods to fine-tune for specific airfields that are known to create bottlenecks, or have reduced airbase efficiency. - -- The more and the longer aircraft need to taxi at an airfield, the more risk there is that: - -- - -- * aircraft will stop waiting for each other or for a landing aircraft before takeoff. - -- * aircraft may get into a "dead-lock" situation, where two aircraft are blocking each other. - -- * aircraft may collide at the airbase. - -- * aircraft may be awaiting the landing of a plane currently in the air, but never lands ... - -- - -- Currently within the DCS engine, the airfield traffic coordination is erroneous and contains a lot of bugs. - -- If you experience while testing problems with aircraft take-off or landing, please use one of the above methods as a solution to workaround these issues! - -- - -- ### 2.2) Planes return near the airbase or will land if damaged. - -- - -- When damaged airplanes return to the airbase, they will be routed and will dissapear in the air when they are near the airbase. - -- There are exceptions to this rule, airplanes that aren't "listening" anymore due to damage or out of fuel, will return to the airbase and land. - -- - -- You can change the way how planes land by using the inherited methods from AI\_A2A\_DISPATCHER: - -- - -- * @{#AI_A2A_DISPATCHER.SetSquadronLanding}() is the generic configuration method to control landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. - -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingNearAirbase}() will despawn the returning aircraft in the air when near the airfield. - -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway. - -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtEngineShutdown}() will despawn the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. - -- - -- You can use these methods to minimize the airbase coodination overhead and to increase the airbase efficiency. - -- When there are lots of aircraft returning for landing, at the same airbase, the takeoff process will be halted, which can cause a complete failure of the - -- A2A defense system, as no new CAP or GCI planes can takeoff. - -- Note that the method @{#AI_A2A_DISPATCHER.SetSquadronLandingNearAirbase}() will only work for returning aircraft, not for damaged or out of fuel aircraft. - -- Damaged or out-of-fuel aircraft are returning to the nearest friendly airbase and will land, and are out of control from ground control. - -- - -- ### 2.3) CAP operations setup for specific airbases, will be executed with the following parameters: - -- - -- * The altitude will range between 6000 and 10000 meters. - -- * The CAP speed will vary between 500 and 800 km/h. - -- * The engage speed between 800 and 1200 km/h. - -- - -- You can change or add a CAP zone by using the inherited methods from AI\_A2A\_DISPATCHER: - -- - -- The method @{#AI_A2A_DISPATCHER.SetSquadronCap}() defines a CAP execution for a squadron. - -- - -- Setting-up a CAP zone also requires specific parameters: - -- - -- * The minimum and maximum altitude - -- * The minimum speed and maximum patrol speed - -- * The minimum and maximum engage speed - -- * The type of altitude measurement - -- - -- These define how the squadron will perform the CAP while partrolling. Different terrain types requires different types of CAP. - -- - -- The @{#AI_A2A_DISPATCHER.SetSquadronCapInterval}() method specifies **how much** and **when** CAP flights will takeoff. - -- - -- It is recommended not to overload the air defense with CAP flights, as these will decrease the performance of the overall system. - -- - -- For example, the following setup will create a CAP for squadron "Sochi": - -- - -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) - -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- - -- ### 2.4) Each airbase will perform GCI when required, with the following parameters: - -- - -- * The engage speed is between 800 and 1200 km/h. - -- - -- You can change or add a GCI parameters by using the inherited methods from AI\_A2A\_DISPATCHER: - -- - -- The method @{#AI_A2A_DISPATCHER.SetSquadronGci}() defines a GCI execution for a squadron. - -- - -- Setting-up a GCI readiness also requires specific parameters: - -- - -- * The minimum speed and maximum patrol speed - -- - -- Essentially this controls how many flights of GCI aircraft can be active at any time. - -- Note allowing large numbers of active GCI flights can adversely impact mission performance on low or medium specification hosts/servers. - -- GCI needs to be setup at strategic airbases. Too far will mean that the aircraft need to fly a long way to reach the intruders, - -- too short will mean that the intruders may have alraedy passed the ideal interception point! - -- - -- For example, the following setup will create a GCI for squadron "Sochi": - -- - -- A2ADispatcher:SetSquadronGci( "Mozdok", 900, 1200 ) - -- - -- ### 2.5) Grouping or detected targets. - -- - -- Detected targets are constantly re-grouped, that is, when certain detected aircraft are moving further than the group radius, then these aircraft will become a separate - -- group being detected. - -- - -- Targets will be grouped within a radius of 30km by default. - -- - -- The radius indicates that detected targets need to be grouped within a radius of 30km. - -- The grouping radius should not be too small, but also depends on the types of planes and the era of the simulation. - -- Fast planes like in the 80s, need a larger radius than WWII planes. - -- Typically I suggest to use 30000 for new generation planes and 10000 for older era aircraft. - -- - -- ## 3) Additional notes: - -- - -- In order to create a two way A2A defense system, **two AI\_A2A\_GCICAP defense systems must need to be created**, for each coalition one. - -- Each defense system needs its own EWR network setup, airplane templates and CAP configurations. - -- - -- This is a good implementation, because maybe in the future, more coalitions may become available in DCS world. - -- - -- ## 4) Coding examples how to use the AI\_A2A\_GCICAP class: - -- - -- ### 4.1) An easy setup: - -- - -- -- Setup the AI_A2A_GCICAP dispatcher for one coalition, and initialize it. - -- GCI_Red = AI_A2A_GCICAP:New( "EWR CCCP", "SQUADRON CCCP", "CAP CCCP", 2 ) - -- -- - -- The following parameters were given to the :New method of AI_A2A_GCICAP, and mean the following: - -- - -- * `"EWR CCCP"`: Groups of the blue coalition are placed that define the EWR network. These groups start with the name `EWR CCCP`. - -- * `"SQUADRON CCCP"`: Late activated Groups objects of the red coalition are placed above the relevant airbases that will contain these templates in the squadron. - -- These late activated Groups start with the name `SQUADRON CCCP`. Each Group object contains only one Unit, and defines the weapon payload, skin and skill level. - -- * `"CAP CCCP"`: CAP Zones are defined using floating, late activated Helicopter Group objects, where the route points define the route of the polygon of the CAP Zone. - -- These Helicopter Group objects start with the name `CAP CCCP`, and will be the locations wherein CAP will be performed. - -- * `2` Defines how many CAP airplanes are patrolling in each CAP zone defined simulateneously. - -- - -- - -- ### 4.2) A more advanced setup: - -- - -- -- Setup the AI_A2A_GCICAP dispatcher for the blue coalition. - -- - -- A2A_GCICAP_Blue = AI_A2A_GCICAP:New( { "BLUE EWR" }, { "104th", "105th", "106th" }, { "104th CAP" }, 4 ) - -- - -- The following parameters for the :New method have the following meaning: - -- - -- * `{ "BLUE EWR" }`: An array of the group name prefixes of the groups of the blue coalition are placed that define the EWR network. These groups start with the name `BLUE EWR`. - -- * `{ "104th", "105th", "106th" } `: An array of the group name prefixes of the Late activated Groups objects of the blue coalition are - -- placed above the relevant airbases that will contain these templates in the squadron. - -- These late activated Groups start with the name `104th` or `105th` or `106th`. - -- * `{ "104th CAP" }`: An array of the names of the CAP zones are defined using floating, late activated helicopter group objects, - -- where the route points define the route of the polygon of the CAP Zone. - -- These Helicopter Group objects start with the name `104th CAP`, and will be the locations wherein CAP will be performed. - -- * `4` Defines how many CAP airplanes are patrolling in each CAP zone defined simulateneously. - -- - -- @field #AI_A2A_GCICAP - AI_A2A_GCICAP = { - ClassName = "AI_A2A_GCICAP", - Detection = nil, - } - - - --- AI_A2A_GCICAP constructor. - -- @param #AI_A2A_GCICAP self - -- @param #string EWRPrefixes A list of prefixes that of groups that setup the Early Warning Radar network. - -- @param #string TemplatePrefixes A list of template prefixes. - -- @param #string CapPrefixes A list of CAP zone prefixes (polygon zones). - -- @param #number CapLimit A number of how many CAP maximum will be spawned. - -- @param #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area. - -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter. - -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task. - -- @param #number GciRadius The radius in meters wherein detected airplanes will GCI. - -- @param #number Resources The amount of resources that will be allocated to each squadron. - -- @return #AI_A2A_GCICAP - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The CAP Zone prefix is "CAP Zone". - -- -- The CAP Limit is 2. - -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2 ) - -- - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The CAP Zone prefix is "CAP Zone". - -- -- The CAP Limit is 2. - -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000 ) - -- - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The CAP Zone prefix is "CAP Zone". - -- -- The CAP Limit is 2. - -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, - -- -- will be considered a defense task if the target is within 60km from the defender. - -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000 ) - -- - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. - -- -- The EWR network group prefix is DF CCCP. All groups starting with DF CCCP will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The CAP Zone prefix is "CAP Zone". - -- -- The CAP Limit is 2. - -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, - -- -- will be considered a defense task if the target is within 60km from the defender. - -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. - -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000, 150000 ) - -- - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object. Each squadron has 30 resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The CAP Zone prefix is "CAP Zone". - -- -- The CAP Limit is 2. - -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, - -- -- will be considered a defense task if the target is within 60km from the defender. - -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. - -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. - -- - -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000, 150000, 30 ) - -- - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object. Each squadron has 30 resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The CAP Zone prefix is nil. No CAP is created. - -- -- The CAP Limit is nil. - -- -- The Grouping Radius is nil. The default range of 6km radius will be grouped as a group of targets. - -- -- The Engage Radius is set nil. The default Engage Radius will be used to consider a defenser being assigned to a task. - -- -- The GCI Radius is nil. Any target detected within the default GCI Radius will be considered for GCI engagement. - -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. - -- - -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, nil, nil, nil, nil, nil, 30 ) - -- - function AI_A2A_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, Resources ) - - local EWRSetGroup = SET_GROUP:New() - EWRSetGroup:FilterPrefixes( EWRPrefixes ) - EWRSetGroup:FilterStart() - - local Detection = DETECTION_AREAS:New( EWRSetGroup, GroupingRadius or 30000 ) - - local self = BASE:Inherit( self, AI_A2A_DISPATCHER:New( Detection ) ) -- #AI_A2A_GCICAP - - self:SetEngageRadius( EngageRadius ) - self:SetGciRadius( GciRadius ) - - -- Determine the coalition of the EWRNetwork, this will be the coalition of the GCICAP. - local EWRFirst = EWRSetGroup:GetFirst() -- Wrapper.Group#GROUP - local EWRCoalition = EWRFirst:GetCoalition() - - -- Determine the airbases belonging to the coalition. - local AirbaseNames = {} -- #list<#string> - for AirbaseID, AirbaseData in pairs( _DATABASE.AIRBASES ) do - local Airbase = AirbaseData -- Wrapper.Airbase#AIRBASE - local AirbaseName = Airbase:GetName() - if Airbase:GetCoalition() == EWRCoalition then - table.insert( AirbaseNames, AirbaseName ) - end - end - - self.Templates = SET_GROUP - :New() - :FilterPrefixes( TemplatePrefixes ) - :FilterOnce() - - -- Setup squadrons - - self:F( { Airbases = AirbaseNames } ) - self.Templates:Flush() - - for AirbaseID, AirbaseName in pairs( AirbaseNames ) do - local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE - local AirbaseName = Airbase:GetName() - local AirbaseCoord = Airbase:GetCoordinate() - local AirbaseZone = ZONE_RADIUS:New( "Airbase", AirbaseCoord:GetVec2(), 3000 ) - local Templates = nil - for TemplateID, Template in pairs( self.Templates:GetSet() ) do - local Template = Template -- Wrapper.Group#GROUP - self:F( { Template = Template:GetName() } ) - local TemplateCoord = Template:GetCoordinate() - if AirbaseZone:IsVec2InZone( TemplateCoord:GetVec2() ) then - Templates = Templates or {} - table.insert( Templates, Template:GetName() ) - end - end - if Templates then - self:SetSquadron( AirbaseName, AirbaseName, Templates, Resources ) - end - end - - -- Setup CAP. - -- Find for each CAP the nearest airbase to the (start or center) of the zone. - -- CAP will be launched from there. - - self.CAPTemplates = SET_GROUP:New() - self.CAPTemplates:FilterPrefixes( CapPrefixes ) - self.CAPTemplates:FilterOnce() - - for CAPID, CAPTemplate in pairs( self.CAPTemplates:GetSet() ) do - local CAPZone = ZONE_POLYGON:New( CAPTemplate:GetName(), CAPTemplate ) - -- Now find the closest airbase from the ZONE (start or center) - local AirbaseDistance = 99999999 - local AirbaseClosest = nil -- Wrapper.Airbase#AIRBASE - for AirbaseID, AirbaseName in pairs( AirbaseNames ) do - local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE - local AirbaseName = Airbase:GetName() - local AirbaseCoord = Airbase:GetCoordinate() - local Squadron = self.DefenderSquadrons[AirbaseName] - if Squadron then - local Distance = AirbaseCoord:Get2DDistance( CAPZone:GetCoordinate() ) - if Distance < AirbaseDistance then - AirbaseDistance = Distance - AirbaseClosest = Airbase - end - end - end - if AirbaseClosest then - self:SetSquadronCap( AirbaseClosest:GetName(), CAPZone, 6000, 10000, 500, 800, 800, 1200, "RADIO" ) - self:SetSquadronCapInterval( AirbaseClosest:GetName(), CapLimit, 300, 600, 1 ) - end - end - - -- Setup GCI. - -- GCI is setup for all Squadrons. - for AirbaseID, AirbaseName in pairs( AirbaseNames ) do - local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE - local AirbaseName = Airbase:GetName() - local Squadron = self.DefenderSquadrons[AirbaseName] - if Squadron then - self:SetSquadronGci( AirbaseName, 800, 1200 ) - end - end - - self:__Start( 5 ) - - self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead ) - self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead ) - - self:HandleEvent( EVENTS.Land ) - self:HandleEvent( EVENTS.EngineShutdown ) - - return self - end - - --- AI_A2A_GCICAP constructor with border. - -- @param #AI_A2A_GCICAP self - -- @param #string EWRPrefixes A list of prefixes that of groups that setup the Early Warning Radar network. - -- @param #string TemplatePrefixes A list of template prefixes. - -- @param #string BorderPrefix A Border Zone Prefix. - -- @param #string CapPrefixes A list of CAP zone prefixes (polygon zones). - -- @param #number CapLimit A number of how many CAP maximum will be spawned. - -- @param #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area. - -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter. - -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task. - -- @param #number GciRadius The radius in meters wherein detected airplanes will GCI. - -- @param #number Resources The amount of resources that will be allocated to each squadron. - -- @return #AI_A2A_GCICAP - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The CAP Zone prefix is "CAP Zone". - -- -- The CAP Limit is 2. - -- - -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2 ) - -- - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. - -- -- The CAP Zone prefix is "CAP Zone". - -- -- The CAP Limit is 2. - -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- - -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000 ) - -- - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. - -- -- The CAP Zone prefix is "CAP Zone". - -- -- The CAP Limit is 2. - -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, - -- -- will be considered a defense task if the target is within 60km from the defender. - -- - -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000 ) - -- - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. - -- -- The CAP Zone prefix is "CAP Zone". - -- -- The CAP Limit is 2. - -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, - -- -- will be considered a defense task if the target is within 60km from the defender. - -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. - -- - -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000, 150000 ) - -- - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has 30 resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. - -- -- The CAP Zone prefix is "CAP Zone". - -- -- The CAP Limit is 2. - -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, - -- -- will be considered a defense task if the target is within 60km from the defender. - -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. - -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. - -- - -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000, 150000, 30 ) - -- - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has 30 resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. - -- -- The CAP Zone prefix is nil. No CAP is created. - -- -- The CAP Limit is nil. - -- -- The Grouping Radius is nil. The default range of 6km radius will be grouped as a group of targets. - -- -- The Engage Radius is set nil. The default Engage Radius will be used to consider a defenser being assigned to a task. - -- -- The GCI Radius is nil. Any target detected within the default GCI Radius will be considered for GCI engagement. - -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. - -- - -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", nil, nil, nil, nil, nil, 30 ) - -- - function AI_A2A_GCICAP:NewWithBorder( EWRPrefixes, TemplatePrefixes, BorderPrefix, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, Resources ) - - local self = AI_A2A_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, Resources ) - - if BorderPrefix then - self:SetBorderZone( ZONE_POLYGON:New( BorderPrefix, GROUP:FindByName( BorderPrefix ) ) ) - end - - return self - - end - -end - ---- **AI** -- **Air Patrolling or Staging.** --- --- ![Banner Image](..\Presentations\AI_PATROL\Dia1.JPG) --- --- === --- --- AI PATROL classes makes AI Controllables execute an Patrol. --- --- There are the following types of PATROL classes defined: --- --- * @{#AI_PATROL_ZONE}: Perform a PATROL in a zone. --- --- ==== --- --- # Demo Missions --- --- ### [AI_PATROL Demo Missions source code](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/PAT%20-%20Patrolling) --- --- ### [AI_PATROL Demo Missions, only for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/PAT%20-%20Patrolling) --- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) --- --- ==== --- --- # YouTube Channel --- --- ### [AI_PATROL YouTube Channel](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl35HvYZKA6G22WMt7iI3zky) --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- ### Contributions: --- --- * **[Dutch_Baron](https://forums.eagle.ru/member.php?u=112075)**: Working together with James has resulted in the creation of the AI_BALANCER class. James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-) --- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Testing and API concept review. --- --- ==== --- --- @module AI_Patrol - - ---- AI_PATROL_ZONE class --- @type AI_PATROL_ZONE --- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Controllable} patrolling. --- @field Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @field Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @field Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @field Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @field Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @field Functional.Spawn#SPAWN CoordTest --- @extends Core.Fsm#FSM_CONTROLLABLE - ---- # AI_PATROL_ZONE class, extends @{Fsm#FSM_CONTROLLABLE} --- --- The AI_PATROL_ZONE class implements the core functions to patrol a @{Zone} by an AI @{Controllable} or @{Group}. --- --- ![Process](..\Presentations\AI_PATROL\Dia3.JPG) --- --- The AI_PATROL_ZONE is assigned a @{Group} and this must be done before the AI_PATROL_ZONE process can be started using the **Start** event. --- --- ![Process](..\Presentations\AI_PATROL\Dia4.JPG) --- --- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. --- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- --- ![Process](..\Presentations\AI_PATROL\Dia5.JPG) --- --- This cycle will continue. --- --- ![Process](..\Presentations\AI_PATROL\Dia6.JPG) --- --- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. --- --- ![Process](..\Presentations\AI_PATROL\Dia9.JPG) --- ----- Note that the enemy is not engaged! To model enemy engagement, either tailor the **Detected** event, or --- use derived AI_ classes to model AI offensive or defensive behaviour. --- --- ![Process](..\Presentations\AI_PATROL\Dia10.JPG) --- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. --- --- ![Process](..\Presentations\AI_PATROL\Dia11.JPG) --- --- ## 1. AI_PATROL_ZONE constructor --- --- * @{#AI_PATROL_ZONE.New}(): Creates a new AI_PATROL_ZONE object. --- --- ## 2. AI_PATROL_ZONE is a FSM --- --- ![Process](..\Presentations\AI_PATROL\Dia2.JPG) --- --- ### 2.1. AI_PATROL_ZONE States --- --- * **None** ( Group ): The process is not started yet. --- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. --- * **Returning** ( Group ): The AI is returning to Base. --- * **Stopped** ( Group ): The process is stopped. --- * **Crashed** ( Group ): The AI has crashed or is dead. --- --- ### 2.2. AI_PATROL_ZONE Events --- --- * **Start** ( Group ): Start the process. --- * **Stop** ( Group ): Stop the process. --- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. --- * **RTB** ( Group ): Route the AI to the home base. --- * **Detect** ( Group ): The AI is detecting targets. --- * **Detected** ( Group ): The AI has detected new targets. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- ## 3. Set or Get the AI controllable --- --- * @{#AI_PATROL_ZONE.SetControllable}(): Set the AIControllable. --- * @{#AI_PATROL_ZONE.GetControllable}(): Get the AIControllable. --- --- ## 4. Set the Speed and Altitude boundaries of the AI controllable --- --- * @{#AI_PATROL_ZONE.SetSpeed}(): Set the patrol speed boundaries of the AI, for the next patrol. --- * @{#AI_PATROL_ZONE.SetAltitude}(): Set altitude boundaries of the AI, for the next patrol. --- --- ## 5. Manage the detection process of the AI controllable --- --- The detection process of the AI controllable can be manipulated. --- Detection requires an amount of CPU power, which has an impact on your mission performance. --- Only put detection on when absolutely necessary, and the frequency of the detection can also be set. --- --- * @{#AI_PATROL_ZONE.SetDetectionOn}(): Set the detection on. The AI will detect for targets. --- * @{#AI_PATROL_ZONE.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased. --- --- The detection frequency can be set with @{#AI_PATROL_ZONE.SetRefreshTimeInterval}( seconds ), where the amount of seconds specify how much seconds will be waited before the next detection. --- Use the method @{#AI_PATROL_ZONE.GetDetectedUnits}() to obtain a list of the @{Unit}s detected by the AI. --- --- The detection can be filtered to potential targets in a specific zone. --- Use the method @{#AI_PATROL_ZONE.SetDetectionZone}() to set the zone where targets need to be detected. --- Note that when the zone is too far away, or the AI is not heading towards the zone, or the AI is too high, no targets may be detected --- according the weather conditions. --- --- ## 6. Manage the "out of fuel" in the AI_PATROL_ZONE --- --- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. --- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. --- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, --- while a new AI is targetted to the AI_PATROL_ZONE. --- Once the time is finished, the old AI will return to the base. --- Use the method @{#AI_PATROL_ZONE.ManageFuel}() to have this proces in place. --- --- ## 7. Manage "damage" behaviour of the AI in the AI_PATROL_ZONE --- --- When the AI is damaged, it is required that a new AIControllable is started. However, damage cannon be foreseen early on. --- Therefore, when the damage treshold is reached, the AI will return immediately to the home base (RTB). --- Use the method @{#AI_PATROL_ZONE.ManageDamage}() to have this proces in place. --- --- === --- --- @field #AI_PATROL_ZONE -AI_PATROL_ZONE = { - ClassName = "AI_PATROL_ZONE", -} - ---- Creates a new AI_PATROL_ZONE object --- @param #AI_PATROL_ZONE self --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO --- @return #AI_PATROL_ZONE self --- @usage --- -- Define a new AI_PATROL_ZONE Object. This PatrolArea will patrol an AIControllable within PatrolZone between 3000 and 6000 meters, with a variying speed between 600 and 900 km/h. --- PatrolZone = ZONE:New( 'PatrolZone' ) --- PatrolSpawn = SPAWN:New( 'Patrol Group' ) --- PatrolArea = AI_PATROL_ZONE:New( PatrolZone, 3000, 6000, 600, 900 ) -function AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_PATROL_ZONE - - - self.PatrolZone = PatrolZone - self.PatrolFloorAltitude = PatrolFloorAltitude - self.PatrolCeilingAltitude = PatrolCeilingAltitude - self.PatrolMinSpeed = PatrolMinSpeed - self.PatrolMaxSpeed = PatrolMaxSpeed - - -- defafult PatrolAltType to "RADIO" if not specified - self.PatrolAltType = PatrolAltType or "RADIO" - - self:SetRefreshTimeInterval( 30 ) - - self.CheckStatus = true - - self:ManageFuel( .2, 60 ) - self:ManageDamage( 1 ) - - - self.DetectedUnits = {} -- This table contains the targets detected during patrol. - - self:SetStartState( "None" ) - - self:AddTransition( "*", "Stop", "Stopped" ) - ---- OnLeave Transition Handler for State Stopped. --- @function [parent=#AI_PATROL_ZONE] OnLeaveStopped --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Stopped. --- @function [parent=#AI_PATROL_ZONE] OnEnterStopped --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- OnBefore Transition Handler for Event Stop. --- @function [parent=#AI_PATROL_ZONE] OnBeforeStop --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Stop. --- @function [parent=#AI_PATROL_ZONE] OnAfterStop --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Stop. --- @function [parent=#AI_PATROL_ZONE] Stop --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Stop. --- @function [parent=#AI_PATROL_ZONE] __Stop --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "None", "Start", "Patrolling" ) - ---- OnBefore Transition Handler for Event Start. --- @function [parent=#AI_PATROL_ZONE] OnBeforeStart --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Start. --- @function [parent=#AI_PATROL_ZONE] OnAfterStart --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Start. --- @function [parent=#AI_PATROL_ZONE] Start --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Start. --- @function [parent=#AI_PATROL_ZONE] __Start --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Patrolling. --- @function [parent=#AI_PATROL_ZONE] OnLeavePatrolling --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Patrolling. --- @function [parent=#AI_PATROL_ZONE] OnEnterPatrolling --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Patrolling", "Route", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event Route. --- @function [parent=#AI_PATROL_ZONE] OnBeforeRoute --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Route. --- @function [parent=#AI_PATROL_ZONE] OnAfterRoute --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Route. --- @function [parent=#AI_PATROL_ZONE] Route --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Route. --- @function [parent=#AI_PATROL_ZONE] __Route --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event Status. --- @function [parent=#AI_PATROL_ZONE] OnBeforeStatus --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Status. --- @function [parent=#AI_PATROL_ZONE] OnAfterStatus --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Status. --- @function [parent=#AI_PATROL_ZONE] Status --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Status. --- @function [parent=#AI_PATROL_ZONE] __Status --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Detect", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event Detect. --- @function [parent=#AI_PATROL_ZONE] OnBeforeDetect --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Detect. --- @function [parent=#AI_PATROL_ZONE] OnAfterDetect --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Detect. --- @function [parent=#AI_PATROL_ZONE] Detect --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Detect. --- @function [parent=#AI_PATROL_ZONE] __Detect --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Detected", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event Detected. --- @function [parent=#AI_PATROL_ZONE] OnBeforeDetected --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Detected. --- @function [parent=#AI_PATROL_ZONE] OnAfterDetected --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Detected. --- @function [parent=#AI_PATROL_ZONE] Detected --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Detected. --- @function [parent=#AI_PATROL_ZONE] __Detected --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "RTB", "Returning" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event RTB. --- @function [parent=#AI_PATROL_ZONE] OnBeforeRTB --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event RTB. --- @function [parent=#AI_PATROL_ZONE] OnAfterRTB --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event RTB. --- @function [parent=#AI_PATROL_ZONE] RTB --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event RTB. --- @function [parent=#AI_PATROL_ZONE] __RTB --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Returning. --- @function [parent=#AI_PATROL_ZONE] OnLeaveReturning --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Returning. --- @function [parent=#AI_PATROL_ZONE] OnEnterReturning --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - - self:AddTransition( "*", "Eject", "*" ) - self:AddTransition( "*", "Crash", "Crashed" ) - self:AddTransition( "*", "PilotDead", "*" ) - - return self -end - - - - ---- Sets (modifies) the minimum and maximum speed of the patrol. --- @param #AI_PATROL_ZONE self --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) - self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) - - self.PatrolMinSpeed = PatrolMinSpeed - self.PatrolMaxSpeed = PatrolMaxSpeed -end - - - ---- Sets the floor and ceiling altitude of the patrol. --- @param #AI_PATROL_ZONE self --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) - self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) - - self.PatrolFloorAltitude = PatrolFloorAltitude - self.PatrolCeilingAltitude = PatrolCeilingAltitude -end - --- * @{#AI_PATROL_ZONE.SetDetectionOn}(): Set the detection on. The AI will detect for targets. --- * @{#AI_PATROL_ZONE.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased. - ---- Set the detection on. The AI will detect for targets. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionOn() - self:F2() - - self.DetectOn = true -end - ---- Set the detection off. The AI will NOT detect for targets. --- However, the list of already detected targets will be kept and can be enquired! --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionOff() - self:F2() - - self.DetectOn = false -end - ---- Set the status checking off. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetStatusOff() - self:F2() - - self.CheckStatus = false -end - ---- Activate the detection. The AI will detect for targets if the Detection is switched On. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionActivated() - self:F2() - - self:ClearDetectedUnits() - self.DetectActivated = true - self:__Detect( -self.DetectInterval ) -end - ---- Deactivate the detection. The AI will NOT detect for targets. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionDeactivated() - self:F2() - - self:ClearDetectedUnits() - self.DetectActivated = false -end - ---- Set the interval in seconds between each detection executed by the AI. --- The list of already detected targets will be kept and updated. --- Newly detected targets will be added, but already detected targets that were --- not detected in this cycle, will NOT be removed! --- The default interval is 30 seconds. --- @param #AI_PATROL_ZONE self --- @param #number Seconds The interval in seconds. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetRefreshTimeInterval( Seconds ) - self:F2() - - if Seconds then - self.DetectInterval = Seconds - else - self.DetectInterval = 30 - end -end - ---- Set the detection zone where the AI is detecting targets. --- @param #AI_PATROL_ZONE self --- @param Core.Zone#ZONE DetectionZone The zone where to detect targets. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionZone( DetectionZone ) - self:F2() - - if DetectionZone then - self.DetectZone = DetectionZone - else - self.DetectZone = nil - end -end - ---- Gets a list of @{Unit#UNIT}s that were detected by the AI. --- No filtering is applied, so, ANY detected UNIT can be in this list. --- It is up to the mission designer to use the @{Unit} class and methods to filter the targets. --- @param #AI_PATROL_ZONE self --- @return #table The list of @{Unit#UNIT}s -function AI_PATROL_ZONE:GetDetectedUnits() - self:F2() - - return self.DetectedUnits -end - ---- Clears the list of @{Unit#UNIT}s that were detected by the AI. --- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:ClearDetectedUnits() - self:F2() - self.DetectedUnits = {} -end - ---- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. --- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. --- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted to the AI_PATROL_ZONE. --- Once the time is finished, the old AI will return to the base. --- @param #AI_PATROL_ZONE self --- @param #number PatrolFuelThresholdPercentage The treshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. --- @param #number PatrolOutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:ManageFuel( PatrolFuelThresholdPercentage, PatrolOutOfFuelOrbitTime ) - - self.PatrolManageFuel = true - self.PatrolFuelThresholdPercentage = PatrolFuelThresholdPercentage - self.PatrolOutOfFuelOrbitTime = PatrolOutOfFuelOrbitTime - - return self -end - ---- When the AI is damaged beyond a certain treshold, it is required that the AI returns to the home base. --- However, damage cannot be foreseen early on. --- Therefore, when the damage treshold is reached, --- the AI will return immediately to the home base (RTB). --- Note that for groups, the average damage of the complete group will be calculated. --- So, in a group of 4 airplanes, 2 lost and 2 with damage 0.2, the damage treshold will be 0.25. --- @param #AI_PATROL_ZONE self --- @param #number PatrolDamageThreshold The treshold in percentage (between 0 and 1) when the AI is considered to be damaged. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:ManageDamage( PatrolDamageThreshold ) - - self.PatrolManageDamage = true - self.PatrolDamageThreshold = PatrolDamageThreshold - - return self -end - ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_PATROL_ZONE:onafterStart( Controllable, From, Event, To ) - self:F2() - - self:__Route( 1 ) -- Route to the patrol point. The asynchronous trigger is important, because a spawned group and units takes at least one second to come live. - self:__Status( 60 ) -- Check status status every 30 seconds. - self:SetDetectionActivated() - - self:HandleEvent( EVENTS.PilotDead, self.OnPilotDead ) - self:HandleEvent( EVENTS.Crash, self.OnCrash ) - self:HandleEvent( EVENTS.Ejection, self.OnEjection ) - - Controllable:OptionROEHoldFire() - Controllable:OptionROTVertical() - - self.Controllable:OnReSpawn( - function( PatrolGroup ) - self:E( "ReSpawn" ) - self:__Reset( 1 ) - self:__Route( 5 ) - end - ) - - self:SetDetectionOn() - -end - - ---- @param #AI_PATROL_ZONE self ---- @param Wrapper.Controllable#CONTROLLABLE Controllable -function AI_PATROL_ZONE:onbeforeDetect( Controllable, From, Event, To ) - - return self.DetectOn and self.DetectActivated -end - ---- @param #AI_PATROL_ZONE self ---- @param Wrapper.Controllable#CONTROLLABLE Controllable -function AI_PATROL_ZONE:onafterDetect( Controllable, From, Event, To ) - - local Detected = false - - local DetectedTargets = Controllable:GetDetectedTargets() - for TargetID, Target in pairs( DetectedTargets or {} ) do - local TargetObject = Target.object - - if TargetObject and TargetObject:isExist() and TargetObject.id_ < 50000000 then - - local TargetUnit = UNIT:Find( TargetObject ) - local TargetUnitName = TargetUnit:GetName() - - if self.DetectionZone then - if TargetUnit:IsInZone( self.DetectionZone ) then - self:T( {"Detected ", TargetUnit } ) - if self.DetectedUnits[TargetUnit] == nil then - self.DetectedUnits[TargetUnit] = true - end - Detected = true - end - else - if self.DetectedUnits[TargetUnit] == nil then - self.DetectedUnits[TargetUnit] = true - end - Detected = true - end - end - end - - self:__Detect( -self.DetectInterval ) - - if Detected == true then - self:__Detected( 1.5 ) - end - -end - ---- @param Wrapper.Controllable#CONTROLLABLE AIControllable --- This statis method is called from the route path within the last task at the last waaypoint of the Controllable. --- Note that this method is required, as triggers the next route when patrolling for the Controllable. -function AI_PATROL_ZONE:_NewPatrolRoute( AIControllable ) - - local PatrolZone = AIControllable:GetState( AIControllable, "PatrolZone" ) -- PatrolCore.Zone#AI_PATROL_ZONE - PatrolZone:__Route( 1 ) -end - - ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) - - self:F2() - - -- When RTB, don't allow anymore the routing. - if From == "RTB" then - return - end - - - if self.Controllable:IsAlive() then - -- Determine if the AIControllable is within the PatrolZone. - -- If not, make a waypoint within the to that the AIControllable will fly at maximum speed to that point. - - local PatrolRoute = {} - - -- Calculate the current route point of the controllable as the start point of the route. - -- However, when the controllable is not in the air, - -- the controllable current waypoint is probably the airbase... - -- Thus, if we would take the current waypoint as the startpoint, upon take-off, the controllable flies - -- immediately back to the airbase, and this is not correct. - -- Therefore, when on a runway, get as the current route point a random point within the PatrolZone. - -- This will make the plane fly immediately to the patrol zone. - - if self.Controllable:InAir() == false then - self:E( "Not in the air, finding route path within PatrolZone" ) - local CurrentVec2 = self.Controllable:GetVec2() - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToPatrolZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TakeOffParking, - POINT_VEC3.RoutePointAction.FromParkingArea, - ToPatrolZoneSpeed, - true - ) - PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint - else - self:E( "In the air, finding route path within PatrolZone" ) - local CurrentVec2 = self.Controllable:GetVec2() - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToPatrolZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToPatrolZoneSpeed, - true - ) - PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint - end - - - --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. - - --- Find a random 2D point in PatrolZone. - local ToTargetVec2 = self.PatrolZone:GetRandomVec2() - self:T2( ToTargetVec2 ) - - --- Define Speed and Altitude. - local ToTargetAltitude = math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) - local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) - self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } ) - - --- Obtain a 3D @{Point} from the 2D point + altitude. - local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y ) - - --- Create a route point of type air. - local ToTargetRoutePoint = ToTargetPointVec3:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - --self.CoordTest:SpawnFromVec3( ToTargetPointVec3:GetVec3() ) - - --ToTargetPointVec3:SmokeRed() - - PatrolRoute[#PatrolRoute+1] = ToTargetRoutePoint - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - self.Controllable:WayPointInitialize( PatrolRoute ) - - --- Do a trick, link the NewPatrolRoute function of the PATROLGROUP object to the AIControllable in a temporary variable ... - self.Controllable:SetState( self.Controllable, "PatrolZone", self ) - self.Controllable:WayPointFunction( #PatrolRoute, 1, "AI_PATROL_ZONE:_NewPatrolRoute" ) - - --- NOW ROUTE THE GROUP! - self.Controllable:WayPointExecute( 1, 2 ) - end - -end - ---- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:onbeforeStatus() - - return self.CheckStatus -end - ---- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:onafterStatus() - self:F2() - - if self.Controllable and self.Controllable:IsAlive() then - - local RTB = false - - local Fuel = self.Controllable:GetUnit(1):GetFuel() - if Fuel < self.PatrolFuelThresholdPercentage then - self:E( self.Controllable:GetName() .. " is out of fuel:" .. Fuel .. ", RTB!" ) - local OldAIControllable = self.Controllable - - local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) - local TimedOrbitTask = OldAIControllable:TaskControlled( OrbitTask, OldAIControllable:TaskCondition(nil,nil,nil,nil,self.PatrolOutOfFuelOrbitTime,nil ) ) - OldAIControllable:SetTask( TimedOrbitTask, 10 ) - - RTB = true - else - end - - -- TODO: Check GROUP damage function. - local Damage = self.Controllable:GetLife() - if Damage <= self.PatrolDamageThreshold then - self:E( self.Controllable:GetName() .. " is damaged:" .. Damage .. ", RTB!" ) - RTB = true - end - - if RTB == true then - self:RTB() - else - self:__Status( 60 ) -- Execute the Patrol event after 30 seconds. - end - end -end - ---- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:onafterRTB() - self:F2() - - if self.Controllable and self.Controllable:IsAlive() then - - self:SetDetectionOff() - self.CheckStatus = false - - local PatrolRoute = {} - - --- Calculate the current route point. - local CurrentVec2 = self.Controllable:GetVec2() - - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToPatrolZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToPatrolZoneSpeed, - true - ) - - PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - self.Controllable:WayPointInitialize( PatrolRoute ) - - --- NOW ROUTE THE GROUP! - self.Controllable:WayPointExecute( 1, 1 ) - - end - -end - ---- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:onafterDead() - self:SetDetectionOff() - self:SetStatusOff() -end - ---- @param #AI_PATROL_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_PATROL_ZONE:OnCrash( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:E( self.Controllable:GetUnits() ) - if #self.Controllable:GetUnits() == 1 then - self:__Crash( 1, EventData ) - end - end -end - ---- @param #AI_PATROL_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_PATROL_ZONE:OnEjection( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:__Eject( 1, EventData ) - end -end - ---- @param #AI_PATROL_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_PATROL_ZONE:OnPilotDead( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:__PilotDead( 1, EventData ) - end -end ---- **AI** -- **Execute Combat Air Patrol (CAP).** --- --- ![Banner Image](..\Presentations\AI_CAP\Dia1.JPG) --- --- === --- --- AI CAP classes makes AI Controllables execute a Combat Air Patrol. --- --- There are the following types of CAP classes defined: --- --- * @{#AI_CAP_ZONE}: Perform a CAP in a zone. --- --- ==== --- --- # Demo Missions --- --- ### [AI_CAP Demo Missions source code](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/CAP%20-%20Combat%20Air%20Patrol) --- --- ### [AI_CAP Demo Missions, only for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/CAP%20-%20Combat%20Air%20Patrol) --- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) --- --- ==== --- --- # YouTube Channel --- --- ### [AI_CAP YouTube Channel](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl1YCyPxJgoZn-CfhwyeW65L) --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- * **[Quax](https://forums.eagle.ru/member.php?u=90530)**: Concept, Advice & Testing. --- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Concept, Advice & Testing. --- * **[Gunterlund](http://forums.eagle.ru:8080/member.php?u=75036)**: Test case revision. --- * **[Whisper](http://forums.eagle.ru/member.php?u=3829): Testing. --- * **[Delta99](https://forums.eagle.ru/member.php?u=125166): Testing. --- --- ==== --- --- @module AI_Cap - - ---- @type AI_CAP_ZONE --- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Controllable} patrolling. --- @field Core.Zone#ZONE_BASE TargetZone The @{Zone} where the patrol needs to be executed. --- @extends AI.AI_Patrol#AI_PATROL_ZONE - - ---- # AI_CAP_ZONE class, extends @{AI_CAP#AI_PATROL_ZONE} --- --- The AI_CAP_ZONE class implements the core functions to patrol a @{Zone} by an AI @{Controllable} or @{Group} --- and automatically engage any airborne enemies that are within a certain range or within a certain zone. --- --- ![Process](..\Presentations\AI_CAP\Dia3.JPG) --- --- The AI_CAP_ZONE is assigned a @{Group} and this must be done before the AI_CAP_ZONE process can be started using the **Start** event. --- --- ![Process](..\Presentations\AI_CAP\Dia4.JPG) --- --- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. --- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- --- ![Process](..\Presentations\AI_CAP\Dia5.JPG) --- --- This cycle will continue. --- --- ![Process](..\Presentations\AI_CAP\Dia6.JPG) --- --- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. --- --- ![Process](..\Presentations\AI_CAP\Dia9.JPG) --- --- When enemies are detected, the AI will automatically engage the enemy. --- --- ![Process](..\Presentations\AI_CAP\Dia10.JPG) --- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. --- --- ![Process](..\Presentations\AI_CAP\Dia13.JPG) --- --- ## 1. AI_CAP_ZONE constructor --- --- * @{#AI_CAP_ZONE.New}(): Creates a new AI_CAP_ZONE object. --- --- ## 2. AI_CAP_ZONE is a FSM --- --- ![Process](..\Presentations\AI_CAP\Dia2.JPG) --- --- ### 2.1 AI_CAP_ZONE States --- --- * **None** ( Group ): The process is not started yet. --- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. --- * **Engaging** ( Group ): The AI is engaging the bogeys. --- * **Returning** ( Group ): The AI is returning to Base.. --- --- ### 2.2 AI_CAP_ZONE Events --- --- * **@{AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. --- * **@{AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. --- * **@{#AI_CAP_ZONE.Engage}**: Let the AI engage the bogeys. --- * **@{#AI_CAP_ZONE.Abort}**: Aborts the engagement and return patrolling in the patrol zone. --- * **@{AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. --- * **@{#AI_CAP_ZONE.Destroy}**: The AI has destroyed a bogey @{Unit}. --- * **@{#AI_CAP_ZONE.Destroyed}**: The AI has destroyed all bogeys @{Unit}s assigned in the CAS task. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- ## 3. Set the Range of Engagement --- --- ![Range](..\Presentations\AI_CAP\Dia11.JPG) --- --- An optional range can be set in meters, --- that will define when the AI will engage with the detected airborne enemy targets. --- The range can be beyond or smaller than the range of the Patrol Zone. --- The range is applied at the position of the AI. --- Use the method @{AI_CAP#AI_CAP_ZONE.SetEngageRange}() to define that range. --- --- ## 4. Set the Zone of Engagement --- --- ![Zone](..\Presentations\AI_CAP\Dia12.JPG) --- --- An optional @{Zone} can be set, --- that will define when the AI will engage with the detected airborne enemy targets. --- Use the method @{AI_Cap#AI_CAP_ZONE.SetEngageZone}() to define that Zone. --- --- === --- --- @field #AI_CAP_ZONE -AI_CAP_ZONE = { - ClassName = "AI_CAP_ZONE", -} - - - ---- Creates a new AI_CAP_ZONE object --- @param #AI_CAP_ZONE self --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO --- @return #AI_CAP_ZONE self -function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_CAP_ZONE - - self.Accomplished = false - self.Engaging = false - - self:AddTransition( { "Patrolling", "Engaging" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. - - --- OnBefore Transition Handler for Event Engage. - -- @function [parent=#AI_CAP_ZONE] OnBeforeEngage - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Engage. - -- @function [parent=#AI_CAP_ZONE] OnAfterEngage - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Engage. - -- @function [parent=#AI_CAP_ZONE] Engage - -- @param #AI_CAP_ZONE self - - --- Asynchronous Event Trigger for Event Engage. - -- @function [parent=#AI_CAP_ZONE] __Engage - -- @param #AI_CAP_ZONE self - -- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Engaging. --- @function [parent=#AI_CAP_ZONE] OnLeaveEngaging --- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Engaging. --- @function [parent=#AI_CAP_ZONE] OnEnterEngaging --- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. - - --- OnBefore Transition Handler for Event Fired. - -- @function [parent=#AI_CAP_ZONE] OnBeforeFired - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Fired. - -- @function [parent=#AI_CAP_ZONE] OnAfterFired - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Fired. - -- @function [parent=#AI_CAP_ZONE] Fired - -- @param #AI_CAP_ZONE self - - --- Asynchronous Event Trigger for Event Fired. - -- @function [parent=#AI_CAP_ZONE] __Fired - -- @param #AI_CAP_ZONE self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. - - --- OnBefore Transition Handler for Event Destroy. - -- @function [parent=#AI_CAP_ZONE] OnBeforeDestroy - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Destroy. - -- @function [parent=#AI_CAP_ZONE] OnAfterDestroy - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_CAP_ZONE] Destroy - -- @param #AI_CAP_ZONE self - - --- Asynchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_CAP_ZONE] __Destroy - -- @param #AI_CAP_ZONE self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. - - --- OnBefore Transition Handler for Event Abort. - -- @function [parent=#AI_CAP_ZONE] OnBeforeAbort - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Abort. - -- @function [parent=#AI_CAP_ZONE] OnAfterAbort - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Abort. - -- @function [parent=#AI_CAP_ZONE] Abort - -- @param #AI_CAP_ZONE self - - --- Asynchronous Event Trigger for Event Abort. - -- @function [parent=#AI_CAP_ZONE] __Abort - -- @param #AI_CAP_ZONE self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. - - --- OnBefore Transition Handler for Event Accomplish. - -- @function [parent=#AI_CAP_ZONE] OnBeforeAccomplish - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Accomplish. - -- @function [parent=#AI_CAP_ZONE] OnAfterAccomplish - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_CAP_ZONE] Accomplish - -- @param #AI_CAP_ZONE self - - --- Asynchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_CAP_ZONE] __Accomplish - -- @param #AI_CAP_ZONE self - -- @param #number Delay The delay in seconds. - - return self -end - - ---- Set the Engage Zone which defines where the AI will engage bogies. --- @param #AI_CAP_ZONE self --- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAP. --- @return #AI_CAP_ZONE self -function AI_CAP_ZONE:SetEngageZone( EngageZone ) - self:F2() - - if EngageZone then - self.EngageZone = EngageZone - else - self.EngageZone = nil - end -end - ---- Set the Engage Range when the AI will engage with airborne enemies. --- @param #AI_CAP_ZONE self --- @param #number EngageRange The Engage Range. --- @return #AI_CAP_ZONE self -function AI_CAP_ZONE:SetEngageRange( EngageRange ) - self:F2() - - if EngageRange then - self.EngageRange = EngageRange - else - self.EngageRange = nil - end -end - ---- onafter State Transition for Event Start. --- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onafterStart( Controllable, From, Event, To ) - - -- Call the parent Start event handler - self:GetParent(self).onafterStart( self, Controllable, From, Event, To ) - self:HandleEvent( EVENTS.Dead ) - -end - - ---- @param AI.AI_CAP#AI_CAP_ZONE --- @param Wrapper.Group#GROUP EngageGroup -function AI_CAP_ZONE.EngageRoute( EngageGroup, Fsm ) - - EngageGroup:F( { "AI_CAP_ZONE.EngageRoute:", EngageGroup:GetName() } ) - - if EngageGroup:IsAlive() then - Fsm:__Engage( 1 ) - end -end - - - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onbeforeEngage( Controllable, From, Event, To ) - - if self.Accomplished == true then - return false - end -end - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onafterDetected( Controllable, From, Event, To ) - - if From ~= "Engaging" then - - local Engage = false - - for DetectedUnit, Detected in pairs( self.DetectedUnits ) do - - local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT - self:T( DetectedUnit ) - if DetectedUnit:IsAlive() and DetectedUnit:IsAir() then - Engage = true - break - end - end - - if Engage == true then - self:F( 'Detected -> Engaging' ) - self:__Engage( 1 ) - end - end -end - - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onafterAbort( Controllable, From, Event, To ) - Controllable:ClearTasks() - self:__Route( 1 ) -end - - - - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onafterEngage( Controllable, From, Event, To ) - - if Controllable:IsAlive() then - - local EngageRoute = {} - - --- Calculate the current route point. - local CurrentVec2 = self.Controllable:GetVec2() - - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToEngageZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToEngageZoneSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = CurrentRoutePoint - - - --- Find a random 2D point in PatrolZone. - local ToTargetVec2 = self.PatrolZone:GetRandomVec2() - self:T2( ToTargetVec2 ) - - --- Define Speed and Altitude. - local ToTargetAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) - local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) - self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } ) - - --- Obtain a 3D @{Point} from the 2D point + altitude. - local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y ) - - --- Create a route point of type air. - local ToPatrolRoutePoint = ToTargetPointVec3:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint - - Controllable:OptionROEOpenFire() - Controllable:OptionROTEvadeFire() - - local AttackTasks = {} - - for DetectedUnit, Detected in pairs( self.DetectedUnits ) do - local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT - self:T( { DetectedUnit, DetectedUnit:IsAlive(), DetectedUnit:IsAir() } ) - if DetectedUnit:IsAlive() and DetectedUnit:IsAir() then - if self.EngageZone then - if DetectedUnit:IsInZone( self.EngageZone ) then - self:F( {"Within Zone and Engaging ", DetectedUnit } ) - AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) - end - else - if self.EngageRange then - if DetectedUnit:GetPointVec3():Get2DDistance(Controllable:GetPointVec3() ) <= self.EngageRange then - self:F( {"Within Range and Engaging", DetectedUnit } ) - AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) - end - else - AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) - end - end - else - self.DetectedUnits[DetectedUnit] = nil - end - end - - if #AttackTasks == 0 then - self:F("No targets found -> Going back to Patrolling") - self:__Abort( 1 ) - self:__Route( 1 ) - self:SetDetectionActivated() - else - - AttackTasks[#AttackTasks+1] = Controllable:TaskFunction( "AI_CAP_ZONE.EngageRoute", self ) - EngageRoute[1].task = Controllable:TaskCombo( AttackTasks ) - - self:SetDetectionDeactivated() - end - - Controllable:Route( EngageRoute, 0.5 ) - - end -end - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onafterAccomplish( Controllable, From, Event, To ) - self.Accomplished = true - self:SetDetectionOff() -end - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @param Core.Event#EVENTDATA EventData -function AI_CAP_ZONE:onafterDestroy( Controllable, From, Event, To, EventData ) - - if EventData.IniUnit then - self.DetectedUnits[EventData.IniUnit] = nil - end -end - ---- @param #AI_CAP_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_CAP_ZONE:OnEventDead( EventData ) - self:F( { "EventDead", EventData } ) - - if EventData.IniDCSUnit then - if self.DetectedUnits and self.DetectedUnits[EventData.IniUnit] then - self:__Destroy( 1, EventData ) - end - end -end ---- **AI** -- **Provide Close Air Support to friendly ground troops.** --- --- ![Banner Image](..\Presentations\AI_CAS\Dia1.JPG) --- --- === --- --- AI CAS classes makes AI Controllables execute a Close Air Support. --- --- There are the following types of CAS classes defined: --- --- * @{#AI_CAS_ZONE}: Perform a CAS in a zone. --- --- ==== --- --- # Demo Missions --- --- ### [AI_CAS Demo Missions source code](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/CAS%20-%20Close%20Air%20Support) --- --- ### [AI_CAS Demo Missions, only for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/CAS%20-%20Close%20Air%20Support) --- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) --- --- ==== --- --- # YouTube Channel --- --- ### [AI_CAS YouTube Channel](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl3JBO1WDqqpyYRRmIkR2ir2) --- --- ==== --- --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- * **[Quax](https://forums.eagle.ru/member.php?u=90530)**: Concept, Advice & Testing. --- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Concept, Advice & Testing. --- * **[Gunterlund](http://forums.eagle.ru:8080/member.php?u=75036)**: Test case revision. --- --- ==== --- --- @module AI_Cas - - ---- AI_CAS_ZONE class --- @type AI_CAS_ZONE --- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Controllable} patrolling. --- @field Core.Zone#ZONE_BASE TargetZone The @{Zone} where the patrol needs to be executed. --- @extends AI.AI_Patrol#AI_PATROL_ZONE - ---- # AI_CAS_ZONE class, extends @{AI_Patrol#AI_PATROL_ZONE} --- --- AI_CAS_ZONE derives from the @{AI_Patrol#AI_PATROL_ZONE}, inheriting its methods and behaviour. --- --- The AI_CAS_ZONE class implements the core functions to provide Close Air Support in an Engage @{Zone} by an AIR @{Controllable} or @{Group}. --- The AI_CAS_ZONE runs a process. It holds an AI in a Patrol Zone and when the AI is commanded to engage, it will fly to an Engage Zone. --- --- ![HoldAndEngage](..\Presentations\AI_CAS\Dia3.JPG) --- --- The AI_CAS_ZONE is assigned a @{Group} and this must be done before the AI_CAS_ZONE process can be started through the **Start** event. --- --- ![Start Event](..\Presentations\AI_CAS\Dia4.JPG) --- --- Upon started, The AI will **Route** itself towards the random 3D point within a patrol zone, --- using a random speed within the given altitude and speed limits. --- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- This cycle will continue until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- --- ![Route Event](..\Presentations\AI_CAS\Dia5.JPG) --- --- When the AI is commanded to provide Close Air Support (through the event **Engage**), the AI will fly towards the Engage Zone. --- Any target that is detected in the Engage Zone will be reported and will be destroyed by the AI. --- --- ![Engage Event](..\Presentations\AI_CAS\Dia6.JPG) --- --- The AI will detect the targets and will only destroy the targets within the Engage Zone. --- --- ![Engage Event](..\Presentations\AI_CAS\Dia7.JPG) --- --- Every target that is destroyed, is reported< by the AI. --- --- ![Engage Event](..\Presentations\AI_CAS\Dia8.JPG) --- --- Note that the AI does not know when the Engage Zone is cleared, and therefore will keep circling in the zone. --- --- ![Engage Event](..\Presentations\AI_CAS\Dia9.JPG) --- --- Until it is notified through the event **Accomplish**, which is to be triggered by an observing party: --- --- * a FAC --- * a timed event --- * a menu option selected by a human --- * a condition --- * others ... --- --- ![Engage Event](..\Presentations\AI_CAS\Dia10.JPG) --- --- When the AI has accomplished the CAS, it will fly back to the Patrol Zone. --- --- ![Engage Event](..\Presentations\AI_CAS\Dia11.JPG) --- --- It will keep patrolling there, until it is notified to RTB or move to another CAS Zone. --- It can be notified to go RTB through the **RTB** event. --- --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. --- --- ![Engage Event](..\Presentations\AI_CAS\Dia12.JPG) --- --- ## AI_CAS_ZONE constructor --- --- * @{#AI_CAS_ZONE.New}(): Creates a new AI_CAS_ZONE object. --- --- ## AI_CAS_ZONE is a FSM --- --- ![Process](..\Presentations\AI_CAS\Dia2.JPG) --- --- ### 2.1. AI_CAS_ZONE States --- --- * **None** ( Group ): The process is not started yet. --- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. --- * **Engaging** ( Group ): The AI is engaging the targets in the Engage Zone, executing CAS. --- * **Returning** ( Group ): The AI is returning to Base.. --- --- ### 2.2. AI_CAS_ZONE Events --- --- * **@{AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. --- * **@{AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. --- * **@{#AI_CAS_ZONE.Engage}**: Engage the AI to provide CAS in the Engage Zone, destroying any target it finds. --- * **@{#AI_CAS_ZONE.Abort}**: Aborts the engagement and return patrolling in the patrol zone. --- * **@{AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. --- * **@{#AI_CAS_ZONE.Destroy}**: The AI has destroyed a target @{Unit}. --- * **@{#AI_CAS_ZONE.Destroyed}**: The AI has destroyed all target @{Unit}s assigned in the CAS task. --- * **Status**: The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- === --- --- @field #AI_CAS_ZONE -AI_CAS_ZONE = { - ClassName = "AI_CAS_ZONE", -} - - - ---- Creates a new AI_CAS_ZONE object --- @param #AI_CAS_ZONE self --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @param Core.Zone#ZONE_BASE EngageZone The zone where the engage will happen. --- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO --- @return #AI_CAS_ZONE self -function AI_CAS_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageZone, PatrolAltType ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_CAS_ZONE - - self.EngageZone = EngageZone - self.Accomplished = false - - self:SetDetectionZone( self.EngageZone ) - - self:AddTransition( { "Patrolling", "Engaging" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Engage. - -- @function [parent=#AI_CAS_ZONE] OnBeforeEngage - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Engage. - -- @function [parent=#AI_CAS_ZONE] OnAfterEngage - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Engage. - -- @function [parent=#AI_CAS_ZONE] Engage - -- @param #AI_CAS_ZONE self - -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. - -- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. - -- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. - -- If parameter is not defined the unit / controllable will choose expend on its own discretion. - -- Use the structure @{DCSTypes#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack. - -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. - -- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. - - --- Asynchronous Event Trigger for Event Engage. - -- @function [parent=#AI_CAS_ZONE] __Engage - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. - -- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. - -- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. - -- If parameter is not defined the unit / controllable will choose expend on its own discretion. - -- Use the structure @{DCSTypes#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack. - -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. - -- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. - ---- OnLeave Transition Handler for State Engaging. --- @function [parent=#AI_CAS_ZONE] OnLeaveEngaging --- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Engaging. --- @function [parent=#AI_CAS_ZONE] OnEnterEngaging --- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Engaging", "Target", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Fired. - -- @function [parent=#AI_CAS_ZONE] OnBeforeFired - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Fired. - -- @function [parent=#AI_CAS_ZONE] OnAfterFired - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Fired. - -- @function [parent=#AI_CAS_ZONE] Fired - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Fired. - -- @function [parent=#AI_CAS_ZONE] __Fired - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Destroy. - -- @function [parent=#AI_CAS_ZONE] OnBeforeDestroy - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Destroy. - -- @function [parent=#AI_CAS_ZONE] OnAfterDestroy - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_CAS_ZONE] Destroy - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_CAS_ZONE] __Destroy - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Abort. - -- @function [parent=#AI_CAS_ZONE] OnBeforeAbort - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Abort. - -- @function [parent=#AI_CAS_ZONE] OnAfterAbort - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Abort. - -- @function [parent=#AI_CAS_ZONE] Abort - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Abort. - -- @function [parent=#AI_CAS_ZONE] __Abort - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Accomplish. - -- @function [parent=#AI_CAS_ZONE] OnBeforeAccomplish - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Accomplish. - -- @function [parent=#AI_CAS_ZONE] OnAfterAccomplish - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_CAS_ZONE] Accomplish - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_CAS_ZONE] __Accomplish - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - - return self -end - - ---- Set the Engage Zone where the AI is performing CAS. Note that if the EngageZone is changed, the AI needs to re-detect targets. --- @param #AI_CAS_ZONE self --- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAS. --- @return #AI_CAS_ZONE self -function AI_CAS_ZONE:SetEngageZone( EngageZone ) - self:F2() - - if EngageZone then - self.EngageZone = EngageZone - else - self.EngageZone = nil - end -end - - - ---- onafter State Transition for Event Start. --- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onafterStart( Controllable, From, Event, To ) - - -- Call the parent Start event handler - self:GetParent(self).onafterStart( self, Controllable, From, Event, To ) - self:HandleEvent( EVENTS.Dead ) - - self:SetDetectionDeactivated() -- When not engaging, set the detection off. -end - ---- @param AI.AI_CAS#AI_CAS_ZONE --- @param Wrapper.Group#GROUP EngageGroup -function AI_CAS_ZONE.EngageRoute( EngageGroup, Fsm ) - - EngageGroup:F( { "AI_CAS_ZONE.EngageRoute:", EngageGroup:GetName() } ) - - if EngageGroup:IsAlive() then - Fsm:__Engage( 1, Fsm.EngageSpeed, Fsm.EngageAltitude, Fsm.EngageWeaponExpend, Fsm.EngageAttackQty, Fsm.EngageDirection ) - end -end - - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onbeforeEngage( Controllable, From, Event, To ) - - if self.Accomplished == true then - return false - end -end - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onafterTarget( Controllable, From, Event, To ) - self:E("onafterTarget") - - if Controllable:IsAlive() then - - local AttackTasks = {} - - for DetectedUnit, Detected in pairs( self.DetectedUnits ) do - local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT - if DetectedUnit:IsAlive() then - if DetectedUnit:IsInZone( self.EngageZone ) then - if Detected == true then - self:E( {"Target: ", DetectedUnit } ) - self.DetectedUnits[DetectedUnit] = false - local AttackTask = Controllable:TaskAttackUnit( DetectedUnit, false, self.EngageWeaponExpend, self.EngageAttackQty, self.EngageDirection, self.EngageAltitude, nil ) - self.Controllable:PushTask( AttackTask, 1 ) - end - end - else - self.DetectedUnits[DetectedUnit] = nil - end - end - - self:__Target( -10 ) - - end -end - - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onafterAbort( Controllable, From, Event, To ) - Controllable:ClearTasks() - self:__Route( 1 ) -end - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. --- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. -function AI_CAS_ZONE:onafterEngage( Controllable, From, Event, To, - EngageSpeed, - EngageAltitude, - EngageWeaponExpend, - EngageAttackQty, - EngageDirection ) - self:F("onafterEngage") - - self.EngageSpeed = EngageSpeed or 400 - self.EngageAltitude = EngageAltitude or 2000 - self.EngageWeaponExpend = EngageWeaponExpend - self.EngageAttackQty = EngageAttackQty - self.EngageDirection = EngageDirection - - if Controllable:IsAlive() then - - Controllable:OptionROEOpenFire() - Controllable:OptionROTVertical() - - local EngageRoute = {} - - --- Calculate the current route point. - local CurrentVec2 = self.Controllable:GetVec2() - - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToEngageZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - self.EngageSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = CurrentRoutePoint - - local AttackTasks = {} - - for DetectedUnit, Detected in pairs( self.DetectedUnits ) do - local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT - self:T( DetectedUnit ) - if DetectedUnit:IsAlive() then - if DetectedUnit:IsInZone( self.EngageZone ) then - self:E( {"Engaging ", DetectedUnit } ) - AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit, - true, - EngageWeaponExpend, - EngageAttackQty, - EngageDirection - ) - end - else - self.DetectedUnits[DetectedUnit] = nil - end - end - - AttackTasks[#AttackTasks+1] = Controllable:TaskFunction( "AI_CAS_ZONE.EngageRoute", self ) - EngageRoute[#EngageRoute].task = Controllable:TaskCombo( AttackTasks ) - - --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. - - --- Find a random 2D point in EngageZone. - local ToTargetVec2 = self.EngageZone:GetRandomVec2() - self:T2( ToTargetVec2 ) - - --- Obtain a 3D @{Point} from the 2D point + altitude. - local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, self.EngageAltitude, ToTargetVec2.y ) - - --- Create a route point of type air. - local ToTargetRoutePoint = ToTargetPointVec3:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - self.EngageSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = ToTargetRoutePoint - - Controllable:Route( EngageRoute, 0.5 ) - - self:SetRefreshTimeInterval( 2 ) - self:SetDetectionActivated() - self:__Target( -2 ) -- Start Targetting - end -end - - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onafterAccomplish( Controllable, From, Event, To ) - self.Accomplished = true - self:SetDetectionDeactivated() -end - - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @param Core.Event#EVENTDATA EventData -function AI_CAS_ZONE:onafterDestroy( Controllable, From, Event, To, EventData ) - - if EventData.IniUnit then - self.DetectedUnits[EventData.IniUnit] = nil - end -end - - ---- @param #AI_CAS_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_CAS_ZONE:OnEventDead( EventData ) - self:F( { "EventDead", EventData } ) - - if EventData.IniDCSUnit then - if self.DetectedUnits and self.DetectedUnits[EventData.IniUnit] then - self:__Destroy( 1, EventData ) - end - end -end - - ---- **AI** -- **Provide Battlefield Air Interdiction (bombing).** --- --- ![Banner Image](..\Presentations\AI_BAI\Dia1.JPG) --- --- === --- --- AI_BAI classes makes AI Controllables execute bombing tasks. --- --- There are the following types of BAI classes defined: --- --- * @{#AI_BAI_ZONE}: Perform a BAI in a zone. --- --- ==== --- --- # Demo Missions --- --- ### [AI_BAI Demo Missions source code](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/BOMB%20-%20Close%20Air%20Support) --- --- ### [AI_BAI Demo Missions, only for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/BOMB%20-%20Close%20Air%20Support) --- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) --- --- ==== --- --- # YouTube Channel --- --- ### [AI_BAI YouTube Channel](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl3JBO1WDqqpyYRRmIkR2ir2) --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- * **[Gunterlund](http://forums.eagle.ru:8080/member.php?u=75036)**: Test case revision. --- --- ==== --- --- @module AI_Bai - - ---- AI_BAI_ZONE class --- @type AI_BAI_ZONE --- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Controllable} patrolling. --- @field Core.Zone#ZONE_BASE TargetZone The @{Zone} where the patrol needs to be executed. --- @extends AI.AI_Patrol#AI_PATROL_ZONE - ---- # AI_BAI_ZONE class, extends @{AI_Patrol#AI_PATROL_ZONE} --- --- AI_BAI_ZONE derives from the @{AI_Patrol#AI_PATROL_ZONE}, inheriting its methods and behaviour. --- --- The AI_BAI_ZONE class implements the core functions to provide BattleGround Air Interdiction in an Engage @{Zone} by an AIR @{Controllable} or @{Group}. --- The AI_BAI_ZONE runs a process. It holds an AI in a Patrol Zone and when the AI is commanded to engage, it will fly to an Engage Zone. --- --- ![HoldAndEngage](..\Presentations\AI_BAI\Dia3.JPG) --- --- The AI_BAI_ZONE is assigned a @{Group} and this must be done before the AI_BAI_ZONE process can be started through the **Start** event. --- --- ![Start Event](..\Presentations\AI_BAI\Dia4.JPG) --- --- Upon started, The AI will **Route** itself towards the random 3D point within a patrol zone, --- using a random speed within the given altitude and speed limits. --- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- This cycle will continue until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- --- ![Route Event](..\Presentations\AI_BAI\Dia5.JPG) --- --- When the AI is commanded to provide BattleGround Air Interdiction (through the event **Engage**), the AI will fly towards the Engage Zone. --- Any target that is detected in the Engage Zone will be reported and will be destroyed by the AI. --- --- ![Engage Event](..\Presentations\AI_BAI\Dia6.JPG) --- --- The AI will detect the targets and will only destroy the targets within the Engage Zone. --- --- ![Engage Event](..\Presentations\AI_BAI\Dia7.JPG) --- --- Every target that is destroyed, is reported< by the AI. --- --- ![Engage Event](..\Presentations\AI_BAI\Dia8.JPG) --- --- Note that the AI does not know when the Engage Zone is cleared, and therefore will keep circling in the zone. --- --- ![Engage Event](..\Presentations\AI_BAI\Dia9.JPG) --- --- Until it is notified through the event **Accomplish**, which is to be triggered by an observing party: --- --- * a FAC --- * a timed event --- * a menu option selected by a human --- * a condition --- * others ... --- --- ![Engage Event](..\Presentations\AI_BAI\Dia10.JPG) --- --- When the AI has accomplished the Bombing, it will fly back to the Patrol Zone. --- --- ![Engage Event](..\Presentations\AI_BAI\Dia11.JPG) --- --- It will keep patrolling there, until it is notified to RTB or move to another BOMB Zone. --- It can be notified to go RTB through the **RTB** event. --- --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. --- --- ![Engage Event](..\Presentations\AI_BAI\Dia12.JPG) --- --- # 1. AI_BAI_ZONE constructor --- --- * @{#AI_BAI_ZONE.New}(): Creates a new AI_BAI_ZONE object. --- --- ## 2. AI_BAI_ZONE is a FSM --- --- ![Process](..\Presentations\AI_BAI\Dia2.JPG) --- --- ### 2.1. AI_BAI_ZONE States --- --- * **None** ( Group ): The process is not started yet. --- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. --- * **Engaging** ( Group ): The AI is engaging the targets in the Engage Zone, executing BOMB. --- * **Returning** ( Group ): The AI is returning to Base.. --- --- ### 2.2. AI_BAI_ZONE Events --- --- * **@{AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. --- * **@{AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. --- * **@{#AI_BAI_ZONE.Engage}**: Engage the AI to provide BOMB in the Engage Zone, destroying any target it finds. --- * **@{#AI_BAI_ZONE.Abort}**: Aborts the engagement and return patrolling in the patrol zone. --- * **@{AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. --- * **@{#AI_BAI_ZONE.Destroy}**: The AI has destroyed a target @{Unit}. --- * **@{#AI_BAI_ZONE.Destroyed}**: The AI has destroyed all target @{Unit}s assigned in the BOMB task. --- * **Status**: The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- ## 3. Modify the Engage Zone behaviour to pinpoint a **map object** or **scenery object** --- --- Use the method @{#AI_BAI_ZONE.SearchOff}() to specify that the EngageZone is not to be searched for potential targets (UNITs), but that the center of the zone --- is the point where a map object is to be destroyed (like a bridge). --- --- Example: --- --- -- Tell the BAI not to search for potential targets in the BAIEngagementZone, but rather use the center of the BAIEngagementZone as the bombing location. --- AIBAIZone:SearchOff() --- --- Searching can be switched back on with the method @{#AI_BAI_ZONE.SearchOn}(). Use the method @{#AI_BAI_ZONE.SearchOnOff}() to flexibily switch searching on or off. --- --- === --- --- @field #AI_BAI_ZONE -AI_BAI_ZONE = { - ClassName = "AI_BAI_ZONE", -} - - - ---- Creates a new AI_BAI_ZONE object --- @param #AI_BAI_ZONE self --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @param Core.Zone#ZONE_BASE EngageZone The zone where the engage will happen. --- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO --- @return #AI_BAI_ZONE self -function AI_BAI_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageZone, PatrolAltType ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_BAI_ZONE - - self.EngageZone = EngageZone - self.Accomplished = false - - self:SetDetectionZone( self.EngageZone ) - self:SearchOn() - - self:AddTransition( { "Patrolling", "Engaging" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_BAI_ZONE. - - --- OnBefore Transition Handler for Event Engage. - -- @function [parent=#AI_BAI_ZONE] OnBeforeEngage - -- @param #AI_BAI_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Engage. - -- @function [parent=#AI_BAI_ZONE] OnAfterEngage - -- @param #AI_BAI_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Engage. - -- @function [parent=#AI_BAI_ZONE] Engage - -- @param #AI_BAI_ZONE self - -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. - -- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. - -- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. - -- If parameter is not defined the unit / controllable will choose expend on its own discretion. - -- Use the structure @{DCSTypes#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack. - -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. - -- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. - - --- Asynchronous Event Trigger for Event Engage. - -- @function [parent=#AI_BAI_ZONE] __Engage - -- @param #AI_BAI_ZONE self - -- @param #number Delay The delay in seconds. - -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. - -- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. - -- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. - -- If parameter is not defined the unit / controllable will choose expend on its own discretion. - -- Use the structure @{DCSTypes#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack. - -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. - -- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. - ---- OnLeave Transition Handler for State Engaging. --- @function [parent=#AI_BAI_ZONE] OnLeaveEngaging --- @param #AI_BAI_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Engaging. --- @function [parent=#AI_BAI_ZONE] OnEnterEngaging --- @param #AI_BAI_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Engaging", "Target", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_BAI_ZONE. - - self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_BAI_ZONE. - - --- OnBefore Transition Handler for Event Fired. - -- @function [parent=#AI_BAI_ZONE] OnBeforeFired - -- @param #AI_BAI_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Fired. - -- @function [parent=#AI_BAI_ZONE] OnAfterFired - -- @param #AI_BAI_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Fired. - -- @function [parent=#AI_BAI_ZONE] Fired - -- @param #AI_BAI_ZONE self - - --- Asynchronous Event Trigger for Event Fired. - -- @function [parent=#AI_BAI_ZONE] __Fired - -- @param #AI_BAI_ZONE self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_BAI_ZONE. - - --- OnBefore Transition Handler for Event Destroy. - -- @function [parent=#AI_BAI_ZONE] OnBeforeDestroy - -- @param #AI_BAI_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Destroy. - -- @function [parent=#AI_BAI_ZONE] OnAfterDestroy - -- @param #AI_BAI_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_BAI_ZONE] Destroy - -- @param #AI_BAI_ZONE self - - --- Asynchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_BAI_ZONE] __Destroy - -- @param #AI_BAI_ZONE self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_BAI_ZONE. - - --- OnBefore Transition Handler for Event Abort. - -- @function [parent=#AI_BAI_ZONE] OnBeforeAbort - -- @param #AI_BAI_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Abort. - -- @function [parent=#AI_BAI_ZONE] OnAfterAbort - -- @param #AI_BAI_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Abort. - -- @function [parent=#AI_BAI_ZONE] Abort - -- @param #AI_BAI_ZONE self - - --- Asynchronous Event Trigger for Event Abort. - -- @function [parent=#AI_BAI_ZONE] __Abort - -- @param #AI_BAI_ZONE self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_BAI_ZONE. - - --- OnBefore Transition Handler for Event Accomplish. - -- @function [parent=#AI_BAI_ZONE] OnBeforeAccomplish - -- @param #AI_BAI_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Accomplish. - -- @function [parent=#AI_BAI_ZONE] OnAfterAccomplish - -- @param #AI_BAI_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_BAI_ZONE] Accomplish - -- @param #AI_BAI_ZONE self - - --- Asynchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_BAI_ZONE] __Accomplish - -- @param #AI_BAI_ZONE self - -- @param #number Delay The delay in seconds. - - return self -end - - ---- Set the Engage Zone where the AI is performing BOMB. Note that if the EngageZone is changed, the AI needs to re-detect targets. --- @param #AI_BAI_ZONE self --- @param Core.Zone#ZONE EngageZone The zone where the AI is performing BOMB. --- @return #AI_BAI_ZONE self -function AI_BAI_ZONE:SetEngageZone( EngageZone ) - self:F2() - - if EngageZone then - self.EngageZone = EngageZone - else - self.EngageZone = nil - end -end - - ---- Specifies whether to search for potential targets in the zone, or let the center of the zone be the bombing coordinate. --- AI_BAI_ZONE will search for potential targets by default. --- @param #AI_BAI_ZONE self --- @return #AI_BAI_ZONE -function AI_BAI_ZONE:SearchOnOff( Search ) - - self.Search = Search - - return self -end - ---- If Search is Off, the current zone coordinate will be the center of the bombing. --- @param #AI_BAI_ZONE self --- @return #AI_BAI_ZONE -function AI_BAI_ZONE:SearchOff() - - self:SearchOnOff( false ) - - return self -end - - ---- If Search is On, BAI will search for potential targets in the zone. --- @param #AI_BAI_ZONE self --- @return #AI_BAI_ZONE -function AI_BAI_ZONE:SearchOn() - - self:SearchOnOff( true ) - - return self -end - - ---- onafter State Transition for Event Start. --- @param #AI_BAI_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_BAI_ZONE:onafterStart( Controllable, From, Event, To ) - - -- Call the parent Start event handler - self:GetParent(self).onafterStart( self, Controllable, From, Event, To ) - self:HandleEvent( EVENTS.Dead ) - - self:SetDetectionDeactivated() -- When not engaging, set the detection off. -end - ---- @param Wrapper.Controllable#CONTROLLABLE AIControllable -function _NewEngageRoute( AIControllable ) - - AIControllable:T( "NewEngageRoute" ) - local EngageZone = AIControllable:GetState( AIControllable, "EngageZone" ) -- AI.AI_BAI#AI_BAI_ZONE - EngageZone:__Engage( 1, EngageZone.EngageSpeed, EngageZone.EngageAltitude, EngageZone.EngageWeaponExpend, EngageZone.EngageAttackQty, EngageZone.EngageDirection ) -end - - ---- @param #AI_BAI_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_BAI_ZONE:onbeforeEngage( Controllable, From, Event, To ) - - if self.Accomplished == true then - return false - end -end - ---- @param #AI_BAI_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_BAI_ZONE:onafterTarget( Controllable, From, Event, To ) - self:F({"onafterTarget",self.Search,Controllable:IsAlive()}) - - - - if Controllable:IsAlive() then - - local AttackTasks = {} - - if self.Search == true then - for DetectedUnit, Detected in pairs( self.DetectedUnits ) do - local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT - if DetectedUnit:IsAlive() then - if DetectedUnit:IsInZone( self.EngageZone ) then - if Detected == true then - self:F( {"Target: ", DetectedUnit } ) - self.DetectedUnits[DetectedUnit] = false - local AttackTask = Controllable:TaskAttackUnit( DetectedUnit, false, self.EngageWeaponExpend, self.EngageAttackQty, self.EngageDirection, self.EngageAltitude, nil ) - self.Controllable:PushTask( AttackTask, 1 ) - end - end - else - self.DetectedUnits[DetectedUnit] = nil - end - end - else - self:F("Attack zone") - local AttackTask = Controllable:TaskAttackMapObject( - self.EngageZone:GetPointVec2():GetVec2(), - true, - self.EngageWeaponExpend, - self.EngageAttackQty, - self.EngageDirection, - self.EngageAltitude - ) - self.Controllable:PushTask( AttackTask, 1 ) - end - - self:__Target( -10 ) - - end -end - - ---- @param #AI_BAI_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_BAI_ZONE:onafterAbort( Controllable, From, Event, To ) - Controllable:ClearTasks() - self:__Route( 1 ) -end - ---- @param #AI_BAI_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. --- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. -function AI_BAI_ZONE:onafterEngage( Controllable, From, Event, To, - EngageSpeed, - EngageAltitude, - EngageWeaponExpend, - EngageAttackQty, - EngageDirection ) - - self:F("onafterEngage") - - self.EngageSpeed = EngageSpeed or 400 - self.EngageAltitude = EngageAltitude or 2000 - self.EngageWeaponExpend = EngageWeaponExpend - self.EngageAttackQty = EngageAttackQty - self.EngageDirection = EngageDirection - - if Controllable:IsAlive() then - - local EngageRoute = {} - - --- Calculate the current route point. - local CurrentVec2 = self.Controllable:GetVec2() - - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToEngageZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - self.EngageSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = CurrentRoutePoint - - local AttackTasks = {} - - if self.Search == true then - - for DetectedUnitID, DetectedUnitData in pairs( self.DetectedUnits ) do - local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT - self:T( DetectedUnit ) - if DetectedUnit:IsAlive() then - if DetectedUnit:IsInZone( self.EngageZone ) then - self:F( {"Engaging ", DetectedUnit } ) - AttackTasks[#AttackTasks+1] = Controllable:TaskBombing( - DetectedUnit:GetPointVec2():GetVec2(), - true, - EngageWeaponExpend, - EngageAttackQty, - EngageDirection, - EngageAltitude - ) - end - else - self.DetectedUnits[DetectedUnit] = nil - end - end - else - self:F("Attack zone") - AttackTasks[#AttackTasks+1] = Controllable:TaskAttackMapObject( - self.EngageZone:GetPointVec2():GetVec2(), - true, - EngageWeaponExpend, - EngageAttackQty, - EngageDirection, - EngageAltitude - ) - end - - EngageRoute[#EngageRoute].task = Controllable:TaskCombo( AttackTasks ) - - --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. - - --- Find a random 2D point in EngageZone. - local ToTargetVec2 = self.EngageZone:GetRandomVec2() - self:T2( ToTargetVec2 ) - - --- Obtain a 3D @{Point} from the 2D point + altitude. - local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, self.EngageAltitude, ToTargetVec2.y ) - - --- Create a route point of type air. - local ToTargetRoutePoint = ToTargetPointVec3:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - self.EngageSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = ToTargetRoutePoint - - Controllable:OptionROEOpenFire() - Controllable:OptionROTVertical() - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - Controllable:WayPointInitialize( EngageRoute ) - - --- Do a trick, link the NewEngageRoute function of the object to the AIControllable in a temporary variable ... - Controllable:SetState( Controllable, "EngageZone", self ) - - Controllable:WayPointFunction( #EngageRoute, 1, "_NewEngageRoute" ) - - --- NOW ROUTE THE GROUP! - Controllable:WayPointExecute( 1 ) - - self:SetRefreshTimeInterval( 2 ) - self:SetDetectionActivated() - self:__Target( -2 ) -- Start Targetting - end -end - - ---- @param #AI_BAI_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_BAI_ZONE:onafterAccomplish( Controllable, From, Event, To ) - self.Accomplished = true - self:SetDetectionDeactivated() -end - - ---- @param #AI_BAI_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @param Core.Event#EVENTDATA EventData -function AI_BAI_ZONE:onafterDestroy( Controllable, From, Event, To, EventData ) - - if EventData.IniUnit then - self.DetectedUnits[EventData.IniUnit] = nil - end -end - - ---- @param #AI_BAI_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_BAI_ZONE:OnEventDead( EventData ) - self:F( { "EventDead", EventData } ) - - if EventData.IniDCSUnit then - if self.DetectedUnits and self.DetectedUnits[EventData.IniUnit] then - self:__Destroy( 1, EventData ) - end - end -end - - ---- **AI** -- Build large **formations** of AI @{Group}s flying together. --- --- ![Banner Image](..\Presentations\AI_FORMATION\Dia1.JPG) --- --- === --- --- AI_FORMATION makes AI @{GROUP}s fly in formation of various compositions. --- The AI_FORMATION class models formations in a different manner than the internal DCS formation logic!!! --- The purpose of the class is to: --- --- * Make formation building a process that can be managed while in flight, rather than a task. --- * Human players can guide formations, consisting of larget planes. --- * Build large formations (like a large bomber field). --- * Form formations that DCS does not support off the shelve. --- --- A few remarks: --- --- * Depending on the type of plane, the change in direction by the leader may result in the formation getting disentangled while in flight and needs to be rebuild. --- * Formations are vulnerable to collissions, but is depending on the type of plane, the distance between the planes and the speed and angle executed by the leader. --- * Formations may take a while to build up. --- --- As a result, the AI_FORMATION is not perfect, but is very useful to: --- --- * Model large formations when flying straight line. --- * Make humans guide a large formation, when the planes are wide from each other. --- --- There are the following types of classes defined: --- --- * @{#AI_FORMATION}: Create a formation from several @{GROUP}s. --- --- ==== --- --- # Demo Missions --- --- ### [AI_FORMATION Demo Missions source code]() --- --- ### [AI_FORMATION Demo Missions, only for beta testers]() --- --- ### [ALL Demo Missions pack of the last release]() --- --- ==== --- --- # YouTube Channel --- ---- ### [AI_FORMATION YouTube Channel]() --- --- === --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- ==== --- --- @module AI_Formation - ---- AI_FORMATION class --- @type AI_FORMATION --- @extends Fsm#FSM_SET --- @field Unit#UNIT FollowUnit --- @field Set#SET_GROUP FollowGroupSet --- @field #string FollowName --- @field #AI_FORMATION.MODE FollowMode The mode the escort is in. --- @field Scheduler#SCHEDULER FollowScheduler The instance of the SCHEDULER class. --- @field #number FollowDistance The current follow distance. --- @field #boolean ReportTargets If true, nearby targets are reported. --- @Field DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the FollowGroup. --- @field DCSTypes#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the FollowGroup. --- @field Menu#MENU_CLIENT FollowMenuResumeMission - - ---- # AI_FORMATION class, extends @{Fsm#FSM_SET} --- --- The #AI_FORMATION class allows you to build large formations, make AI follow a @{Client#CLIENT} (player) leader or a @{Unit#UNIT} (AI) leader. --- --- AI_FORMATION makes AI @{GROUP}s fly in formation of various compositions. --- The AI_FORMATION class models formations in a different manner than the internal DCS formation logic!!! --- The purpose of the class is to: --- --- * Make formation building a process that can be managed while in flight, rather than a task. --- * Human players can guide formations, consisting of larget planes. --- * Build large formations (like a large bomber field). --- * Form formations that DCS does not support off the shelve. --- --- A few remarks: --- --- * Depending on the type of plane, the change in direction by the leader may result in the formation getting disentangled while in flight and needs to be rebuild. --- * Formations are vulnerable to collissions, but is depending on the type of plane, the distance between the planes and the speed and angle executed by the leader. --- * Formations may take a while to build up. --- --- As a result, the AI_FORMATION is not perfect, but is very useful to: --- --- * Model large formations when flying straight line. You can build close formations when doing this. --- * Make humans guide a large formation, when the planes are wide from each other. --- --- ## AI_FORMATION construction --- --- Create a new SPAWN object with the @{#AI_FORMATION.New} method: --- --- * @{Follow#AI_FORMATION.New}(): Creates a new AI_FORMATION object from a @{Group#GROUP} for a @{Client#CLIENT} or a @{Unit#UNIT}, with an optional briefing text. --- --- ## Formation methods --- --- The following methods can be used to set or change the formation: --- --- * @{AI_Formation#AI_FORMATION.FormationLine}(): Form a line formation (core formation function). --- * @{AI_Formation#AI_FORMATION.FormationTrail}(): Form a trail formation. --- * @{AI_Formation#AI_FORMATION.FormationLeftLine}(): Form a left line formation. --- * @{AI_Formation#AI_FORMATION.FormationRightLine}(): Form a right line formation. --- * @{AI_Formation#AI_FORMATION.FormationRightWing}(): Form a right wing formation. --- * @{AI_Formation#AI_FORMATION.FormationLeftWing}(): Form a left wing formation. --- * @{AI_Formation#AI_FORMATION.FormationCenterWing}(): Form a center wing formation. --- * @{AI_Formation#AI_FORMATION.FormationCenterVic}(): Form a Vic formation (same as CenterWing. --- * @{AI_Formation#AI_FORMATION.FormationCenterBoxed}(): Form a center boxed formation. --- --- ## Randomization --- --- Use the method @{AI_Formation#AI_FORMATION.SetFlightRandomization}() to simulate the formation flying errors that pilots make while in formation. Is a range set in meters. --- --- @usage --- local FollowGroupSet = SET_GROUP:New():FilterCategories("plane"):FilterCoalitions("blue"):FilterPrefixes("Follow"):FilterStart() --- FollowGroupSet:Flush() --- local LeaderUnit = UNIT:FindByName( "Leader" ) --- local LargeFormation = AI_FORMATION:New( LeaderUnit, FollowGroupSet, "Center Wing Formation", "Briefing" ) --- LargeFormation:FormationCenterWing( 500, 50, 0, 250, 250 ) --- LargeFormation:__Start( 1 ) --- --- @field #AI_FORMATION -AI_FORMATION = { - ClassName = "AI_FORMATION", - FollowName = nil, -- The Follow Name - FollowUnit = nil, - FollowGroupSet = nil, - FollowMode = 1, - MODE = { - FOLLOW = 1, - MISSION = 2, - }, - FollowScheduler = nil, - OptionROE = AI.Option.Air.val.ROE.OPEN_FIRE, - OptionReactionOnThreat = AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION, -} - ---- AI_FORMATION.Mode class --- @type AI_FORMATION.MODE --- @field #number FOLLOW --- @field #number MISSION - ---- MENUPARAM type --- @type MENUPARAM --- @field #AI_FORMATION ParamSelf --- @field #Distance ParamDistance --- @field #function ParamFunction --- @field #string ParamMessage - ---- AI_FORMATION class constructor for an AI group --- @param #AI_FORMATION self --- @param Unit#UNIT FollowUnit The UNIT leading the FolllowGroupSet. --- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. --- @param #string FollowName Name of the escort. --- @return #AI_FORMATION self -function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefing ) --R2.1 - local self = BASE:Inherit( self, FSM_SET:New( FollowGroupSet ) ) - self:F( { FollowUnit, FollowGroupSet, FollowName } ) - - self.FollowUnit = FollowUnit -- Unit#UNIT - self.FollowGroupSet = FollowGroupSet -- Set#SET_GROUP - - self:SetFlightRandomization( 2 ) - - self:SetStartState( "None" ) - - self:AddTransition( "*", "Stop", "Stopped" ) - - self:AddTransition( "None", "Start", "Following" ) - - self:AddTransition( "*", "FormationLine", "*" ) - --- FormationLine Handler OnBefore for AI_FORMATION - -- @function [parent=#AI_FORMATION] OnBeforeFormationLine - -- @param #AI_FORMATION self - -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #nubmer YSpace The space between groups on the Y-axis in meters for each sequent group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - -- @return #boolean - - --- FormationLine Handler OnAfter for AI_FORMATION - -- @function [parent=#AI_FORMATION] OnAfterFormationLine - -- @param #AI_FORMATION self - -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #nubmer YSpace The space between groups on the Y-axis in meters for each sequent group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - --- FormationLine Trigger for AI_FORMATION - -- @function [parent=#AI_FORMATION] FormationLine - -- @param #AI_FORMATION self - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #nubmer YSpace The space between groups on the Y-axis in meters for each sequent group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - --- FormationLine Asynchronous Trigger for AI_FORMATION - -- @function [parent=#AI_FORMATION] __FormationLine - -- @param #AI_FORMATION self - -- @param #number Delay - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #nubmer YSpace The space between groups on the Y-axis in meters for each sequent group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - self:AddTransition( "*", "FormationTrail", "*" ) - --- FormationTrail Handler OnBefore for AI_FORMATION - -- @function [parent=#AI_FORMATION] OnBeforeFormationTrail - -- @param #AI_FORMATION self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @return #boolean - - --- FormationTrail Handler OnAfter for AI_FORMATION - -- @function [parent=#AI_FORMATION] OnAfterFormationTrail - -- @param #AI_FORMATION self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - - --- FormationTrail Trigger for AI_FORMATION - -- @function [parent=#AI_FORMATION] FormationTrail - -- @param #AI_FORMATION self - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - - --- FormationTrail Asynchronous Trigger for AI_FORMATION - -- @function [parent=#AI_FORMATION] __FormationTrail - -- @param #AI_FORMATION self - -- @param #number Delay - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - - self:AddTransition( "*", "FormationStack", "*" ) - --- FormationStack Handler OnBefore for AI_FORMATION - -- @function [parent=#AI_FORMATION] OnBeforeFormationStack - -- @param #AI_FORMATION self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. - -- @return #boolean - - --- FormationStack Handler OnAfter for AI_FORMATION - -- @function [parent=#AI_FORMATION] OnAfterFormationStack - -- @param #AI_FORMATION self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. - - --- FormationStack Trigger for AI_FORMATION - -- @function [parent=#AI_FORMATION] FormationStack - -- @param #AI_FORMATION self - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. - - --- FormationStack Asynchronous Trigger for AI_FORMATION - -- @function [parent=#AI_FORMATION] __FormationStack - -- @param #AI_FORMATION self - -- @param #number Delay - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. - - self:AddTransition( "*", "FormationLeftLine", "*" ) - --- FormationLeftLine Handler OnBefore for AI_FORMATION - -- @function [parent=#AI_FORMATION] OnBeforeFormationLeftLine - -- @param #AI_FORMATION self - -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - -- @return #boolean - - --- FormationLeftLine Handler OnAfter for AI_FORMATION - -- @function [parent=#AI_FORMATION] OnAfterFormationLeftLine - -- @param #AI_FORMATION self - -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - --- FormationLeftLine Trigger for AI_FORMATION - -- @function [parent=#AI_FORMATION] FormationLeftLine - -- @param #AI_FORMATION self - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - --- FormationLeftLine Asynchronous Trigger for AI_FORMATION - -- @function [parent=#AI_FORMATION] __FormationLeftLine - -- @param #AI_FORMATION self - -- @param #number Delay - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - self:AddTransition( "*", "FormationRightLine", "*" ) - --- FormationRightLine Handler OnBefore for AI_FORMATION - -- @function [parent=#AI_FORMATION] OnBeforeFormationRightLine - -- @param #AI_FORMATION self - -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - -- @return #boolean - - --- FormationRightLine Handler OnAfter for AI_FORMATION - -- @function [parent=#AI_FORMATION] OnAfterFormationRightLine - -- @param #AI_FORMATION self - -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - --- FormationRightLine Trigger for AI_FORMATION - -- @function [parent=#AI_FORMATION] FormationRightLine - -- @param #AI_FORMATION self - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - --- FormationRightLine Asynchronous Trigger for AI_FORMATION - -- @function [parent=#AI_FORMATION] __FormationRightLine - -- @param #AI_FORMATION self - -- @param #number Delay - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - self:AddTransition( "*", "FormationLeftWing", "*" ) - --- FormationLeftWing Handler OnBefore for AI_FORMATION - -- @function [parent=#AI_FORMATION] OnBeforeFormationLeftWing - -- @param #AI_FORMATION self - -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - -- @return #boolean - - --- FormationLeftWing Handler OnAfter for AI_FORMATION - -- @function [parent=#AI_FORMATION] OnAfterFormationLeftWing - -- @param #AI_FORMATION self - -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - --- FormationLeftWing Trigger for AI_FORMATION - -- @function [parent=#AI_FORMATION] FormationLeftWing - -- @param #AI_FORMATION self - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - --- FormationLeftWing Asynchronous Trigger for AI_FORMATION - -- @function [parent=#AI_FORMATION] __FormationLeftWing - -- @param #AI_FORMATION self - -- @param #number Delay - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - self:AddTransition( "*", "FormationRightWing", "*" ) - --- FormationRightWing Handler OnBefore for AI_FORMATION - -- @function [parent=#AI_FORMATION] OnBeforeFormationRightWing - -- @param #AI_FORMATION self - -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - -- @return #boolean - - --- FormationRightWing Handler OnAfter for AI_FORMATION - -- @function [parent=#AI_FORMATION] OnAfterFormationRightWing - -- @param #AI_FORMATION self - -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - --- FormationRightWing Trigger for AI_FORMATION - -- @function [parent=#AI_FORMATION] FormationRightWing - -- @param #AI_FORMATION self - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - --- FormationRightWing Asynchronous Trigger for AI_FORMATION - -- @function [parent=#AI_FORMATION] __FormationRightWing - -- @param #AI_FORMATION self - -- @param #number Delay - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - self:AddTransition( "*", "FormationCenterWing", "*" ) - --- FormationCenterWing Handler OnBefore for AI_FORMATION - -- @function [parent=#AI_FORMATION] OnBeforeFormationCenterWing - -- @param #AI_FORMATION self - -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - -- @return #boolean - - --- FormationCenterWing Handler OnAfter for AI_FORMATION - -- @function [parent=#AI_FORMATION] OnAfterFormationCenterWing - -- @param #AI_FORMATION self - -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - --- FormationCenterWing Trigger for AI_FORMATION - -- @function [parent=#AI_FORMATION] FormationCenterWing - -- @param #AI_FORMATION self - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - --- FormationCenterWing Asynchronous Trigger for AI_FORMATION - -- @function [parent=#AI_FORMATION] __FormationCenterWing - -- @param #AI_FORMATION self - -- @param #number Delay - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - self:AddTransition( "*", "FormationVic", "*" ) - --- FormationVic Handler OnBefore for AI_FORMATION - -- @function [parent=#AI_FORMATION] OnBeforeFormationVic - -- @param #AI_FORMATION self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - -- @return #boolean - - --- FormationVic Handler OnAfter for AI_FORMATION - -- @function [parent=#AI_FORMATION] OnAfterFormationVic - -- @param #AI_FORMATION self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - --- FormationVic Trigger for AI_FORMATION - -- @function [parent=#AI_FORMATION] FormationVic - -- @param #AI_FORMATION self - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - --- FormationVic Asynchronous Trigger for AI_FORMATION - -- @function [parent=#AI_FORMATION] __FormationVic - -- @param #AI_FORMATION self - -- @param #number Delay - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - - self:AddTransition( "*", "FormationBox", "*" ) - --- FormationBox Handler OnBefore for AI_FORMATION - -- @function [parent=#AI_FORMATION] OnBeforeFormationBox - -- @param #AI_FORMATION self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - -- @param #number ZLevels The amount of levels on the Z-axis. - -- @return #boolean - - --- FormationBox Handler OnAfter for AI_FORMATION - -- @function [parent=#AI_FORMATION] OnAfterFormationBox - -- @param #AI_FORMATION self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - -- @param #number ZLevels The amount of levels on the Z-axis. - - --- FormationBox Trigger for AI_FORMATION - -- @function [parent=#AI_FORMATION] FormationBox - -- @param #AI_FORMATION self - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - -- @param #number ZLevels The amount of levels on the Z-axis. - - --- FormationBox Asynchronous Trigger for AI_FORMATION - -- @function [parent=#AI_FORMATION] __FormationBox - -- @param #AI_FORMATION self - -- @param #number Delay - -- @param #number XStart The start position on the X-axis in meters for the first group. - -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. - -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. - -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. - -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - -- @param #number ZLevels The amount of levels on the Z-axis. - - - self:AddTransition( "*", "Follow", "Following" ) - - self:FormationLeftLine( 500, 0, 250, 250 ) - - self.FollowName = FollowName - self.FollowBriefing = FollowBriefing - - - self.CT1 = 0 - self.GT1 = 0 - - self.FollowMode = AI_FORMATION.MODE.MISSION - - return self -end - ---- This function is for test, it will put on the frequency of the FollowScheduler a red smoke at the direction vector calculated for the escort to fly to. --- This allows to visualize where the escort is flying to. --- @param #AI_FORMATION self --- @param #boolean SmokeDirection If true, then the direction vector will be smoked. --- @return #AI_FORMATION -function AI_FORMATION:TestSmokeDirectionVector( SmokeDirection ) --R2.1 - self.SmokeDirectionVector = ( SmokeDirection == true ) and true or false - return self -end - ---- FormationLine Handler OnAfter for AI_FORMATION --- @param #AI_FORMATION self --- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. --- @param #string From --- @param #string Event --- @param #string To --- @param #number XStart The start position on the X-axis in meters for the first group. --- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. --- @param #nubmer YStart The start position on the Y-axis in meters for the first group. --- @param #nubmer YSpace The space between groups on the Y-axis in meters for each sequent group. --- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. --- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. --- @return #AI_FORMATION -function AI_FORMATION:onafterFormationLine( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) --R2.1 - self:F( { FollowGroupSet, From , Event ,To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace } ) - - FollowGroupSet:Flush() - - local FollowSet = FollowGroupSet:GetSet() - - local i = 0 - - for FollowID, FollowGroup in pairs( FollowSet ) do - - local PointVec3 = POINT_VEC3:New() - PointVec3:SetX( XStart + i * XSpace ) - PointVec3:SetY( YStart + i * YSpace ) - PointVec3:SetZ( ZStart + i * ZSpace ) - - local Vec3 = PointVec3:GetVec3() - FollowGroup:SetState( self, "FormationVec3", Vec3 ) - i = i + 1 - end - - return self - -end - ---- FormationTrail Handler OnAfter for AI_FORMATION --- @param #AI_FORMATION self --- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. --- @param #string From --- @param #string Event --- @param #string To --- @param #number XStart The start position on the X-axis in meters for the first group. --- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. --- @param #nubmer YStart The start position on the Y-axis in meters for the first group. --- @return #AI_FORMATION -function AI_FORMATION:onafterFormationTrail( FollowGroupSet, From , Event , To, XStart, XSpace, YStart ) --R2.1 - - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,0,0) - - return self -end - - ---- FormationStack Handler OnAfter for AI_FORMATION --- @param #AI_FORMATION self --- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. --- @param #string From --- @param #string Event --- @param #string To --- @param #number XStart The start position on the X-axis in meters for the first group. --- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. --- @param #nubmer YStart The start position on the Y-axis in meters for the first group. --- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. --- @return #AI_FORMATION -function AI_FORMATION:onafterFormationStack( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace ) --R2.1 - - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,0,0) - - return self -end - - - - ---- FormationLeftLine Handler OnAfter for AI_FORMATION --- @param #AI_FORMATION self --- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. --- @param #string From --- @param #string Event --- @param #string To --- @param #number XStart The start position on the X-axis in meters for the first group. --- @param #nubmer YStart The start position on the Y-axis in meters for the first group. --- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. --- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. --- @return #AI_FORMATION -function AI_FORMATION:onafterFormationLeftLine( FollowGroupSet, From , Event , To, XStart, YStart, ZStart, ZSpace ) --R2.1 - - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,ZStart,ZSpace) - - return self -end - - ---- FormationRightLine Handler OnAfter for AI_FORMATION --- @param #AI_FORMATION self --- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. --- @param #string From --- @param #string Event --- @param #string To --- @param #number XStart The start position on the X-axis in meters for the first group. --- @param #nubmer YStart The start position on the Y-axis in meters for the first group. --- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. --- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. --- @return #AI_FORMATION -function AI_FORMATION:onafterFormationRightLine( FollowGroupSet, From , Event , To, XStart, YStart, ZStart, ZSpace ) --R2.1 - - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,-ZStart,-ZSpace) - - return self -end - - ---- FormationLeftWing Handler OnAfter for AI_FORMATION --- @param #AI_FORMATION self --- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. --- @param #string From --- @param #string Event --- @param #string To --- @param #number XStart The start position on the X-axis in meters for the first group. --- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. --- @param #nubmer YStart The start position on the Y-axis in meters for the first group. --- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. --- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. -function AI_FORMATION:onafterFormationLeftWing( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, ZStart, ZSpace ) --R2.1 - - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,ZStart,ZSpace) - - return self -end - - ---- FormationRightWing Handler OnAfter for AI_FORMATION --- @function [parent=#AI_FORMATION] OnAfterFormationRightWing --- @param #AI_FORMATION self --- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. --- @param #string From --- @param #string Event --- @param #string To --- @param #number XStart The start position on the X-axis in meters for the first group. --- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. --- @param #nubmer YStart The start position on the Y-axis in meters for the first group. --- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. --- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. -function AI_FORMATION:onafterFormationRightWing( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, ZStart, ZSpace ) --R2.1 - - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,-ZStart,-ZSpace) - - return self -end - - ---- FormationCenterWing Handler OnAfter for AI_FORMATION --- @param #AI_FORMATION self --- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. --- @param #string From --- @param #string Event --- @param #string To --- @param #number XStart The start position on the X-axis in meters for the first group. --- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. --- @param #nubmer YStart The start position on the Y-axis in meters for the first group. --- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. --- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. --- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. -function AI_FORMATION:onafterFormationCenterWing( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) --R2.1 - - local FollowSet = FollowGroupSet:GetSet() - - local i = 0 - - for FollowID, FollowGroup in pairs( FollowSet ) do - - local PointVec3 = POINT_VEC3:New() - - local Side = ( i % 2 == 0 ) and 1 or -1 - local Row = i / 2 + 1 - - PointVec3:SetX( XStart + Row * XSpace ) - PointVec3:SetY( YStart ) - PointVec3:SetZ( Side * ( ZStart + i * ZSpace ) ) - - local Vec3 = PointVec3:GetVec3() - FollowGroup:SetState( self, "FormationVec3", Vec3 ) - i = i + 1 - end - - return self -end - - ---- FormationVic Handle for AI_FORMATION --- @param #AI_FORMATION self --- @param #string From --- @param #string Event --- @param #string To --- @param #number XStart The start position on the X-axis in meters for the first group. --- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. --- @param #nubmer YStart The start position on the Y-axis in meters for the first group. --- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. --- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. --- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. --- @return #AI_FORMATION -function AI_FORMATION:onafterFormationVic( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) --R2.1 - - self:onafterFormationCenterWing(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,ZStart,ZSpace) - - return self -end - ---- FormationBox Handler OnAfter for AI_FORMATION --- @param #AI_FORMATION self --- @param #string From --- @param #string Event --- @param #string To --- @param #number XStart The start position on the X-axis in meters for the first group. --- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. --- @param #nubmer YStart The start position on the Y-axis in meters for the first group. --- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. --- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. --- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. --- @param #number ZLevels The amount of levels on the Z-axis. --- @return #AI_FORMATION -function AI_FORMATION:onafterFormationBox( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) --R2.1 - - local FollowSet = FollowGroupSet:GetSet() - - local i = 0 - - for FollowID, FollowGroup in pairs( FollowSet ) do - - local PointVec3 = POINT_VEC3:New() - - local ZIndex = i % ZLevels - local XIndex = math.floor( i / ZLevels ) - local YIndex = math.floor( i / ZLevels ) - - PointVec3:SetX( XStart + XIndex * XSpace ) - PointVec3:SetY( YStart + YIndex * YSpace ) - PointVec3:SetZ( -ZStart - (ZSpace * ZLevels / 2 ) + ZSpace * ZIndex ) - - local Vec3 = PointVec3:GetVec3() - FollowGroup:SetState( self, "FormationVec3", Vec3 ) - i = i + 1 - end - - return self -end - - ---- Use the method @{AI_Formation#AI_FORMATION.SetFlightRandomization}() to make the air units in your formation randomize their flight a bit while in formation. --- @param #AI_FORMATION self --- @param #number FlightRandomization The formation flying errors that pilots can make while in formation. Is a range set in meters. --- @return #AI_FORMATION -function AI_FORMATION:SetFlightRandomization( FlightRandomization ) --R2.1 - - self.FlightRandomization = FlightRandomization - - return self -end - - ---- @param Follow#AI_FORMATION self -function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 - self:F( ) - - self:T( { self.FollowUnit.UnitName, self.FollowUnit:IsAlive() } ) - if self.FollowUnit:IsAlive() then - - local ClientUnit = self.FollowUnit - - self:T( {ClientUnit.UnitName } ) - - local CT1, CT2, CV1, CV2 - CT1 = ClientUnit:GetState( self, "CT1" ) - - if CT1 == nil or CT1 == 0 then - ClientUnit:SetState( self, "CV1", ClientUnit:GetPointVec3() ) - ClientUnit:SetState( self, "CT1", timer.getTime() ) - else - CT1 = ClientUnit:GetState( self, "CT1" ) - CT2 = timer.getTime() - CV1 = ClientUnit:GetState( self, "CV1" ) - CV2 = ClientUnit:GetPointVec3() - - ClientUnit:SetState( self, "CT1", CT2 ) - ClientUnit:SetState( self, "CV1", CV2 ) - end - - FollowGroupSet:ForEachGroup( - --- @param Wrapper.Group#GROUP FollowGroup - -- @param Wrapper.Unit#UNIT ClientUnit - function( FollowGroup, Formation, ClientUnit, CT1, CV1, CT2, CV2 ) - - FollowGroup:OptionROTEvadeFire() - FollowGroup:OptionROEReturnFire() - - local GroupUnit = FollowGroup:GetUnit( 1 ) - local FollowFormation = FollowGroup:GetState( self, "FormationVec3" ) - if FollowFormation then - local FollowDistance = FollowFormation.x - - local GT1 = GroupUnit:GetState( self, "GT1" ) - - if CT1 == nil or CT1 == 0 or GT1 == nil or GT1 == 0 then - GroupUnit:SetState( self, "GV1", GroupUnit:GetPointVec3() ) - GroupUnit:SetState( self, "GT1", timer.getTime() ) - else - local CD = ( ( CV2.x - CV1.x )^2 + ( CV2.y - CV1.y )^2 + ( CV2.z - CV1.z )^2 ) ^ 0.5 - local CT = CT2 - CT1 - - local CS = ( 3600 / CT ) * ( CD / 1000 ) / 3.6 - - local CDv = { x = CV2.x - CV1.x, y = CV2.y - CV1.y, z = CV2.z - CV1.z } - local Ca = math.atan2( CDv.x, CDv.z ) - - local GT1 = GroupUnit:GetState( self, "GT1" ) - local GT2 = timer.getTime() - local GV1 = GroupUnit:GetState( self, "GV1" ) - local GV2 = GroupUnit:GetPointVec3() - GV2:AddX( math.random( -Formation.FlightRandomization / 2, Formation.FlightRandomization / 2 ) ) - GV2:AddY( math.random( -Formation.FlightRandomization / 2, Formation.FlightRandomization / 2 ) ) - GV2:AddZ( math.random( -Formation.FlightRandomization / 2, Formation.FlightRandomization / 2 ) ) - GroupUnit:SetState( self, "GT1", GT2 ) - GroupUnit:SetState( self, "GV1", GV2 ) - - - local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5 - local GT = GT2 - GT1 - - - -- Calculate the distance - local GDv = { x = GV2.x - CV1.x, y = GV2.y - CV1.y, z = GV2.z - CV1.z } - local Alpha_T = math.atan2( GDv.x, GDv.z ) - math.atan2( CDv.x, CDv.z ) - local Alpha_R = ( Alpha_T < 0 ) and Alpha_T + 2 * math.pi or Alpha_T - local Position = math.cos( Alpha_R ) - local GD = ( ( GDv.x )^2 + ( GDv.z )^2 ) ^ 0.5 - local Distance = GD * Position + - CS * 0,5 - - -- Calculate the group direction vector - local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z } - - -- Calculate GH2, GH2 with the same height as CV2. - local GH2 = { x = GV2.x, y = CV2.y + FollowFormation.y, z = GV2.z } - - -- Calculate the angle of GV to the orthonormal plane - local alpha = math.atan2( GV.x, GV.z ) - - local GVx = FollowFormation.z * math.cos( Ca ) + FollowFormation.x * math.sin( Ca ) - local GVz = FollowFormation.x * math.cos( Ca ) - FollowFormation.z * math.sin( Ca ) - - - -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. - -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) - local CVI = { x = CV2.x + CS * 10 * math.sin(Ca), - y = GH2.y - ( Distance + FollowFormation.x ) / 5, -- + FollowFormation.y, - z = CV2.z + CS * 10 * math.cos(Ca), - } - - -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction. - local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z } - - -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s. - -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed. - -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right... - local DVu = { x = DV.x / FollowDistance, y = DV.y, z = DV.z / FollowDistance } - - -- Now we can calculate the group destination vector GDV. - local GDV = { x = CVI.x, y = CVI.y, z = CVI.z } - - local ADDx = FollowFormation.x * math.cos(alpha) - FollowFormation.z * math.sin(alpha) - local ADDz = FollowFormation.z * math.cos(alpha) + FollowFormation.x * math.sin(alpha) - - local GDV_Formation = { - x = GDV.x - GVx, - y = GDV.y, - z = GDV.z - GVz - } - - if self.SmokeDirectionVector == true then - trigger.action.smoke( GDV, trigger.smokeColor.Green ) - trigger.action.smoke( GDV_Formation, trigger.smokeColor.White ) - end - - - - local Time = 60 - - local Speed = - ( Distance + FollowFormation.x ) / Time - local GS = Speed + CS - if Speed < 0 then - Speed = 0 - end - - -- Now route the escort to the desired point with the desired speed. - FollowGroup:RouteToVec3( GDV_Formation, GS ) -- DCS models speed in Mps (Miles per second) - end - end - end, - self, ClientUnit, CT1, CV1, CT2, CV2 - ) - - self:__Follow( -0.5 ) - end - -end - ---- (SP) (MP) (FSM) Accept or reject process for player (task) assignments. --- --- === --- --- # @{#ACT_ASSIGN} FSM template class, extends @{Fsm#FSM_PROCESS} --- --- ## ACT_ASSIGN state machine: --- --- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. --- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, --- but will have **different implementation behaviour** upon each event or state transition. --- --- ### ACT_ASSIGN **Events**: --- --- These are the events defined in this class: --- --- * **Start**: Start the tasking acceptance process. --- * **Assign**: Assign the task. --- * **Reject**: Reject the task.. --- --- ### ACT_ASSIGN **Event methods**: --- --- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. --- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: --- --- * **Immediate**: The event method has exactly the name of the event. --- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. --- --- ### ACT_ASSIGN **States**: --- --- * **UnAssigned**: The player has not accepted the task. --- * **Assigned (*)**: The player has accepted the task. --- * **Rejected (*)**: The player has not accepted the task. --- * **Waiting**: The process is awaiting player feedback. --- * **Failed (*)**: The process has failed. --- --- (*) End states of the process. --- --- ### ACT_ASSIGN state transition methods: --- --- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. --- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Before** the state transition. --- The state transition method needs to start with the name **OnBefore + the name of the state**. --- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, --- but then you'll need to specify your own logic using the AIControllable! --- --- * **After** the state transition. --- The state transition method needs to start with the name **OnAfter + the name of the state**. --- These state transition methods need to provide a return value, which is specified at the function description. --- --- === --- --- # 1) @{#ACT_ASSIGN_ACCEPT} class, extends @{Fsm.Assign#ACT_ASSIGN} --- --- The ACT_ASSIGN_ACCEPT class accepts by default a task for a player. No player intervention is allowed to reject the task. --- --- ## 1.1) ACT_ASSIGN_ACCEPT constructor: --- --- * @{#ACT_ASSIGN_ACCEPT.New}(): Creates a new ACT_ASSIGN_ACCEPT object. --- --- === --- --- # 2) @{#ACT_ASSIGN_MENU_ACCEPT} class, extends @{Fsm.Assign#ACT_ASSIGN} --- --- The ACT_ASSIGN_MENU_ACCEPT class accepts a task when the player accepts the task through an added menu option. --- This assignment type is useful to conditionally allow the player to choose whether or not he would accept the task. --- The assignment type also allows to reject the task. --- --- ## 2.1) ACT_ASSIGN_MENU_ACCEPT constructor: --- ----------------------------------------- --- --- * @{#ACT_ASSIGN_MENU_ACCEPT.New}(): Creates a new ACT_ASSIGN_MENU_ACCEPT object. --- --- === --- --- @module Assign - - -do -- ACT_ASSIGN - - --- ACT_ASSIGN class - -- @type ACT_ASSIGN - -- @field Tasking.Task#TASK Task - -- @field Wrapper.Unit#UNIT ProcessUnit - -- @field Core.Zone#ZONE_BASE TargetZone - -- @extends Core.Fsm#FSM_PROCESS - ACT_ASSIGN = { - ClassName = "ACT_ASSIGN", - } - - - --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted. - -- @param #ACT_ASSIGN self - -- @return #ACT_ASSIGN The task acceptance process. - function ACT_ASSIGN:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ASSIGN" ) ) -- Core.Fsm#FSM_PROCESS - - self:AddTransition( "UnAssigned", "Start", "Waiting" ) - self:AddTransition( "Waiting", "Assign", "Assigned" ) - self:AddTransition( "Waiting", "Reject", "Rejected" ) - self:AddTransition( "*", "Fail", "Failed" ) - - self:AddEndState( "Assigned" ) - self:AddEndState( "Rejected" ) - self:AddEndState( "Failed" ) - - self:SetStartState( "UnAssigned" ) - - return self - end - -end -- ACT_ASSIGN - - - -do -- ACT_ASSIGN_ACCEPT - - --- ACT_ASSIGN_ACCEPT class - -- @type ACT_ASSIGN_ACCEPT - -- @field Tasking.Task#TASK Task - -- @field Wrapper.Unit#UNIT ProcessUnit - -- @field Core.Zone#ZONE_BASE TargetZone - -- @extends #ACT_ASSIGN - ACT_ASSIGN_ACCEPT = { - ClassName = "ACT_ASSIGN_ACCEPT", - } - - - --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted. - -- @param #ACT_ASSIGN_ACCEPT self - -- @param #string TaskBriefing - function ACT_ASSIGN_ACCEPT:New( TaskBriefing ) - - local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_ACCEPT - - self.TaskBriefing = TaskBriefing - - return self - end - - function ACT_ASSIGN_ACCEPT:Init( FsmAssign ) - - self.TaskBriefing = FsmAssign.TaskBriefing - end - - --- StateMachine callback function - -- @param #ACT_ASSIGN_ACCEPT self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIGN_ACCEPT:onafterStart( ProcessUnit, From, Event, To ) - self:E( { ProcessUnit, From, Event, To } ) - - self:__Assign( 1 ) - end - - --- StateMachine callback function - -- @param #ACT_ASSIGN_ACCEPT self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIGN_ACCEPT:onenterAssigned( ProcessUnit, From, Event, To ) - env.info( "in here" ) - self:E( { ProcessUnit, From, Event, To } ) - - local ProcessGroup = ProcessUnit:GetGroup() - - self.Task:Assign( ProcessUnit, ProcessUnit:GetPlayerName() ) - end - -end -- ACT_ASSIGN_ACCEPT - - -do -- ACT_ASSIGN_MENU_ACCEPT - - --- ACT_ASSIGN_MENU_ACCEPT class - -- @type ACT_ASSIGN_MENU_ACCEPT - -- @field Tasking.Task#TASK Task - -- @field Wrapper.Unit#UNIT ProcessUnit - -- @field Core.Zone#ZONE_BASE TargetZone - -- @extends #ACT_ASSIGN - ACT_ASSIGN_MENU_ACCEPT = { - ClassName = "ACT_ASSIGN_MENU_ACCEPT", - } - - --- Init. - -- @param #ACT_ASSIGN_MENU_ACCEPT self - -- @param #string TaskName - -- @param #string TaskBriefing - -- @return #ACT_ASSIGN_MENU_ACCEPT self - function ACT_ASSIGN_MENU_ACCEPT:New( TaskName, TaskBriefing ) - - -- Inherits from BASE - local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_MENU_ACCEPT - - self.TaskName = TaskName - self.TaskBriefing = TaskBriefing - - return self - end - - function ACT_ASSIGN_MENU_ACCEPT:Init( FsmAssign ) - - self.TaskName = FsmAssign.TaskName - self.TaskBriefing = FsmAssign.TaskBriefing - end - - - --- Creates a new task assignment state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. - -- @param #ACT_ASSIGN_MENU_ACCEPT self - -- @param #string TaskName - -- @param #string TaskBriefing - -- @return #ACT_ASSIGN_MENU_ACCEPT self - function ACT_ASSIGN_MENU_ACCEPT:Init( TaskName, TaskBriefing ) - - self.TaskBriefing = TaskBriefing - self.TaskName = TaskName - - return self - end - - --- StateMachine callback function - -- @param #ACT_ASSIGN_MENU_ACCEPT self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIGN_MENU_ACCEPT:onafterStart( ProcessUnit, From, Event, To ) - self:E( { ProcessUnit, From, Event, To } ) - - self:GetCommandCenter():MessageTypeToGroup( "Access the radio menu to accept the task. You have 30 seconds or the assignment will be cancelled.", ProcessUnit:GetGroup(), MESSAGE.Type.Information ) - - local ProcessGroup = ProcessUnit:GetGroup() - - self.Menu = MENU_GROUP:New( ProcessGroup, "Task " .. self.TaskName .. " acceptance" ) - self.MenuAcceptTask = MENU_GROUP_COMMAND:New( ProcessGroup, "Accept task " .. self.TaskName, self.Menu, self.MenuAssign, self ) - self.MenuRejectTask = MENU_GROUP_COMMAND:New( ProcessGroup, "Reject task " .. self.TaskName, self.Menu, self.MenuReject, self ) - end - - --- Menu function. - -- @param #ACT_ASSIGN_MENU_ACCEPT self - function ACT_ASSIGN_MENU_ACCEPT:MenuAssign() - self:E( ) - - self:__Assign( 1 ) - end - - --- Menu function. - -- @param #ACT_ASSIGN_MENU_ACCEPT self - function ACT_ASSIGN_MENU_ACCEPT:MenuReject() - self:E( ) - - self:__Reject( 1 ) - end - - --- StateMachine callback function - -- @param #ACT_ASSIGN_MENU_ACCEPT self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIGN_MENU_ACCEPT:onafterAssign( ProcessUnit, From, Event, To ) - self:E( { ProcessUnit.UnitNameFrom, Event, To } ) - - self.Menu:Remove() - end - - --- StateMachine callback function - -- @param #ACT_ASSIGN_MENU_ACCEPT self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIGN_MENU_ACCEPT:onafterReject( ProcessUnit, From, Event, To ) - self:E( { ProcessUnit.UnitName, From, Event, To } ) - - self.Menu:Remove() - --TODO: need to resolve this problem ... it has to do with the events ... - --self.Task:UnAssignFromUnit( ProcessUnit )needs to become a callback funtion call upon the event - ProcessUnit:Destroy() - end - -end -- ACT_ASSIGN_MENU_ACCEPT ---- (SP) (MP) (FSM) Route AI or players through waypoints or to zones. --- --- === --- --- # @{#ACT_ROUTE} FSM class, extends @{Fsm#FSM_PROCESS} --- --- ## ACT_ROUTE state machine: --- --- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. --- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, --- but will have **different implementation behaviour** upon each event or state transition. --- --- ### ACT_ROUTE **Events**: --- --- These are the events defined in this class: --- --- * **Start**: The process is started. The process will go into the Report state. --- * **Report**: The process is reporting to the player the route to be followed. --- * **Route**: The process is routing the controllable. --- * **Pause**: The process is pausing the route of the controllable. --- * **Arrive**: The controllable has arrived at a route point. --- * **More**: There are more route points that need to be followed. The process will go back into the Report state. --- * **NoMore**: There are no more route points that need to be followed. The process will go into the Success state. --- --- ### ACT_ROUTE **Event methods**: --- --- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. --- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: --- --- * **Immediate**: The event method has exactly the name of the event. --- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. --- --- ### ACT_ROUTE **States**: --- --- * **None**: The controllable did not receive route commands. --- * **Arrived (*)**: The controllable has arrived at a route point. --- * **Aborted (*)**: The controllable has aborted the route path. --- * **Routing**: The controllable is understay to the route point. --- * **Pausing**: The process is pausing the routing. AI air will go into hover, AI ground will stop moving. Players can fly around. --- * **Success (*)**: All route points were reached. --- * **Failed (*)**: The process has failed. --- --- (*) End states of the process. --- --- ### ACT_ROUTE state transition methods: --- --- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. --- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Before** the state transition. --- The state transition method needs to start with the name **OnBefore + the name of the state**. --- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, --- but then you'll need to specify your own logic using the AIControllable! --- --- * **After** the state transition. --- The state transition method needs to start with the name **OnAfter + the name of the state**. --- These state transition methods need to provide a return value, which is specified at the function description. --- --- === --- --- # 1) @{#ACT_ROUTE_ZONE} class, extends @{Fsm.Route#ACT_ROUTE} --- --- The ACT_ROUTE_ZONE class implements the core functions to route an AIR @{Controllable} player @{Unit} to a @{Zone}. --- The player receives on perioding times messages with the coordinates of the route to follow. --- Upon arrival at the zone, a confirmation of arrival is sent, and the process will be ended. --- --- # 1.1) ACT_ROUTE_ZONE constructor: --- --- * @{#ACT_ROUTE_ZONE.New}(): Creates a new ACT_ROUTE_ZONE object. --- --- === --- --- @module Route - - -do -- ACT_ROUTE - - --- ACT_ROUTE class - -- @type ACT_ROUTE - -- @field Tasking.Task#TASK TASK - -- @field Wrapper.Unit#UNIT ProcessUnit - -- @field Core.Zone#ZONE_BASE Zone - -- @field Core.Point#COORDINATE Coordinate - -- @extends Core.Fsm#FSM_PROCESS - ACT_ROUTE = { - ClassName = "ACT_ROUTE", - } - - - --- Creates a new routing state machine. The process will route a CLIENT to a ZONE until the CLIENT is within that ZONE. - -- @param #ACT_ROUTE self - -- @return #ACT_ROUTE self - function ACT_ROUTE:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ROUTE" ) ) -- Core.Fsm#FSM_PROCESS - - self:AddTransition( "*", "Reset", "None" ) - self:AddTransition( "None", "Start", "Routing" ) - self:AddTransition( "*", "Report", "*" ) - self:AddTransition( "Routing", "Route", "Routing" ) - self:AddTransition( "Routing", "Pause", "Pausing" ) - self:AddTransition( "Routing", "Arrive", "Arrived" ) - self:AddTransition( "*", "Cancel", "Cancelled" ) - self:AddTransition( "Arrived", "Success", "Success" ) - self:AddTransition( "*", "Fail", "Failed" ) - self:AddTransition( "", "", "" ) - self:AddTransition( "", "", "" ) - - self:AddEndState( "Arrived" ) - self:AddEndState( "Failed" ) - self:AddEndState( "Cancelled" ) - - self:SetStartState( "None" ) - - self:SetRouteMode( "C" ) - - return self - end - - --- Set a Cancel Menu item. - -- @param #ACT_ROUTE self - -- @return #ACT_ROUTE - function ACT_ROUTE:SetMenuCancel( MenuGroup, MenuText, ParentMenu, MenuTime ) - - MENU_GROUP_COMMAND:New( - MenuGroup, - MenuText, - ParentMenu, - self.MenuCancel, - self - ):SetTime(MenuTime) - - return self - end - - --- Set the route mode. - -- There are 2 route modes supported: - -- - -- * SetRouteMode( "B" ): Route mode is Bearing and Range. - -- * SetRouteMode( "C" ): Route mode is LL or MGRS according coordinate system setup. - -- - -- @param #ACT_ROUTE self - -- @return #ACT_ROUTE - function ACT_ROUTE:SetRouteMode( RouteMode ) - - self.RouteMode = RouteMode - - return self - end - - --- Get the routing text to be displayed. - -- The route mode determines the text displayed. - -- @param #ACT_ROUTE self - -- @param Wrapper.Unit#UNIT Controllable - -- @return #string - function ACT_ROUTE:GetRouteText( Controllable ) - - self:E() - - local RouteText = "" - - local Coordinate = nil -- Core.Point#COORDINATE - - if self.Coordinate then - Coordinate = self.Coordinate - end - - if self.Zone then - Coordinate = self.Zone:GetPointVec3( self.Altitude ) - Coordinate:SetHeading( self.Heading ) - end - - - local Task = self:GetTask() -- This is to dermine that the coordinates are for a specific task mode (A2A or A2G). - local CC = self:GetTask():GetMission():GetCommandCenter() - if CC then - if CC:IsModeWWII() then - -- Find closest reference point to the target. - local ShortestDistance = 0 - local ShortestReferencePoint = nil - local ShortestReferenceName = "" - self:E( { CC.ReferencePoints } ) - for ZoneName, Zone in pairs( CC.ReferencePoints ) do - self:E( { ZoneName = ZoneName } ) - local Zone = Zone -- Core.Zone#ZONE - local ZoneCoord = Zone:GetCoordinate() - local ZoneDistance = ZoneCoord:Get2DDistance( self.Coordinate ) - self:E( { ShortestDistance, ShortestReferenceName } ) - if ShortestDistance == 0 or ZoneDistance < ShortestDistance then - ShortestDistance = ZoneDistance - ShortestReferencePoint = ZoneCoord - ShortestReferenceName = CC.ReferenceNames[ZoneName] - end - end - if ShortestReferencePoint then - RouteText = Coordinate:ToStringFromRP( ShortestReferencePoint, ShortestReferenceName, Controllable ) - end - else - RouteText = Coordinate:ToString( Controllable, nil, Task ) - end - end - - return RouteText - end - - - function ACT_ROUTE:MenuCancel() - self:Cancel() - end - - --- Task Events - - --- StateMachine callback function - -- @param #ACT_ROUTE self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ROUTE:onafterStart( ProcessUnit, From, Event, To ) - - - self:__Route( 1 ) - end - - --- Check if the controllable has arrived. - -- @param #ACT_ROUTE self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @return #boolean - function ACT_ROUTE:onfuncHasArrived( ProcessUnit ) - return false - end - - --- StateMachine callback function - -- @param #ACT_ROUTE self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ROUTE:onbeforeRoute( ProcessUnit, From, Event, To ) - self:F( { "BeforeRoute 1", self.DisplayCount, self.DisplayInterval } ) - - if ProcessUnit:IsAlive() then - self:F( "BeforeRoute 2" ) - local HasArrived = self:onfuncHasArrived( ProcessUnit ) -- Polymorphic - if self.DisplayCount >= self.DisplayInterval then - self:T( { HasArrived = HasArrived } ) - if not HasArrived then - self:Report() - end - self.DisplayCount = 1 - else - self.DisplayCount = self.DisplayCount + 1 - end - - self:T( { DisplayCount = self.DisplayCount } ) - - if HasArrived then - self:__Arrive( 1 ) - else - self:__Route( 1 ) - end - - return HasArrived -- if false, then the event will not be executed... - end - - return false - - end - -end -- ACT_ROUTE - - -do -- ACT_ROUTE_POINT - - --- ACT_ROUTE_POINT class - -- @type ACT_ROUTE_POINT - -- @field Tasking.Task#TASK TASK - -- @extends #ACT_ROUTE - ACT_ROUTE_POINT = { - ClassName = "ACT_ROUTE_POINT", - } - - - --- Creates a new routing state machine. - -- The task will route a controllable to a Coordinate until the controllable is within the Range. - -- @param #ACT_ROUTE_POINT self - -- @param Core.Point#COORDINATE The Coordinate to Target. - -- @param #number Range The Distance to Target. - -- @param Core.Zone#ZONE_BASE Zone - function ACT_ROUTE_POINT:New( Coordinate, Range ) - local self = BASE:Inherit( self, ACT_ROUTE:New() ) -- #ACT_ROUTE_POINT - - self.Coordinate = Coordinate - self.Range = Range or 0 - - self.DisplayInterval = 30 - self.DisplayCount = 30 - self.DisplayMessage = true - self.DisplayTime = 10 -- 10 seconds is the default - - return self - end - - --- Creates a new routing state machine. - -- The task will route a controllable to a Coordinate until the controllable is within the Range. - -- @param #ACT_ROUTE_POINT self - function ACT_ROUTE_POINT:Init( FsmRoute ) - - self.Coordinate = FsmRoute.Coordinate - self.Range = FsmRoute.Range or 0 - - self.DisplayInterval = 30 - self.DisplayCount = 30 - self.DisplayMessage = true - self.DisplayTime = 10 -- 10 seconds is the default - self:SetStartState("None") - end - - --- Set Coordinate - -- @param #ACT_ROUTE_POINT self - -- @param Core.Point#COORDINATE Coordinate The Coordinate to route to. - function ACT_ROUTE_POINT:SetCoordinate( Coordinate ) - self:F2( { Coordinate } ) - self.Coordinate = Coordinate - end - - --- Get Coordinate - -- @param #ACT_ROUTE_POINT self - -- @return Core.Point#COORDINATE Coordinate The Coordinate to route to. - function ACT_ROUTE_POINT:GetCoordinate() - self:F2( { self.Coordinate } ) - return self.Coordinate - end - - --- Set Range around Coordinate - -- @param #ACT_ROUTE_POINT self - -- @param #number Range The Range to consider the arrival. Default is 10000 meters. - function ACT_ROUTE_POINT:SetRange( Range ) - self:F2( { self.Range } ) - self.Range = Range or 10000 - end - - --- Get Range around Coordinate - -- @param #ACT_ROUTE_POINT self - -- @return #number The Range to consider the arrival. Default is 10000 meters. - function ACT_ROUTE_POINT:GetRange() - return self.Range - end - - --- Method override to check if the controllable has arrived. - -- @param #ACT_ROUTE_POINT self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @return #boolean - function ACT_ROUTE_POINT:onfuncHasArrived( ProcessUnit ) - - if ProcessUnit:IsAlive() then - local Distance = self.Coordinate:Get2DDistance( ProcessUnit:GetCoordinate() ) - - if Distance <= self.Range then - local RouteText = "You have arrived." - self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) - return true - end - end - - return false - end - - --- Task Events - - --- StateMachine callback function - -- @param #ACT_ROUTE_POINT self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ROUTE_POINT:onafterReport( ProcessUnit, From, Event, To ) - - local RouteText = self:GetRouteText( ProcessUnit ) - - self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Update ) - end - -end -- ACT_ROUTE_POINT - - -do -- ACT_ROUTE_ZONE - - --- ACT_ROUTE_ZONE class - -- @type ACT_ROUTE_ZONE - -- @field Tasking.Task#TASK TASK - -- @field Wrapper.Unit#UNIT ProcessUnit - -- @field Core.Zone#ZONE_BASE Zone - -- @extends #ACT_ROUTE - ACT_ROUTE_ZONE = { - ClassName = "ACT_ROUTE_ZONE", - } - - - --- Creates a new routing state machine. The task will route a controllable to a ZONE until the controllable is within that ZONE. - -- @param #ACT_ROUTE_ZONE self - -- @param Core.Zone#ZONE_BASE Zone - function ACT_ROUTE_ZONE:New( Zone ) - local self = BASE:Inherit( self, ACT_ROUTE:New() ) -- #ACT_ROUTE_ZONE - - self.Zone = Zone - - self.DisplayInterval = 30 - self.DisplayCount = 30 - self.DisplayMessage = true - self.DisplayTime = 10 -- 10 seconds is the default - - return self - end - - function ACT_ROUTE_ZONE:Init( FsmRoute ) - - self.Zone = FsmRoute.Zone - - self.DisplayInterval = 30 - self.DisplayCount = 30 - self.DisplayMessage = true - self.DisplayTime = 10 -- 10 seconds is the default - end - - --- Set Zone - -- @param #ACT_ROUTE_ZONE self - -- @param Core.Zone#ZONE_BASE Zone The Zone object where to route to. - -- @param #number Altitude - -- @param #number Heading - function ACT_ROUTE_ZONE:SetZone( Zone, Altitude, Heading ) -- R2.2 Added altitude and heading - self.Zone = Zone - self.Altitude = Altitude - self.Heading = Heading - end - - --- Get Zone - -- @param #ACT_ROUTE_ZONE self - -- @return Core.Zone#ZONE_BASE Zone The Zone object where to route to. - function ACT_ROUTE_ZONE:GetZone() - return self.Zone - end - - --- Method override to check if the controllable has arrived. - -- @param #ACT_ROUTE self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @return #boolean - function ACT_ROUTE_ZONE:onfuncHasArrived( ProcessUnit ) - - if ProcessUnit:IsInZone( self.Zone ) then - local RouteText = "You have arrived within the zone." - self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) - end - - return ProcessUnit:IsInZone( self.Zone ) - end - - --- Task Events - - --- StateMachine callback function - -- @param #ACT_ROUTE_ZONE self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ROUTE_ZONE:onafterReport( ProcessUnit, From, Event, To ) - self:E( { ProcessUnit = ProcessUnit } ) - - local RouteText = self:GetRouteText( ProcessUnit ) - self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Update ) - end - -end -- ACT_ROUTE_ZONE ---- **Actions** - ACT_ACCOUNT_ classes **account for** (detect, count & report) various DCS events occuring on @{Unit}s. --- --- ![Banner Image](..\Presentations\ACT_ACCOUNT\Dia1.JPG) --- --- === --- --- @module Account - - -do -- ACT_ACCOUNT - - --- # @{#ACT_ACCOUNT} FSM class, extends @{Fsm#FSM_PROCESS} - -- - -- ## ACT_ACCOUNT state machine: - -- - -- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. - -- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. - -- Each derived class follows exactly the same process, using the same events and following the same state transitions, - -- but will have **different implementation behaviour** upon each event or state transition. - -- - -- ### ACT_ACCOUNT States - -- - -- * **Asigned**: The player is assigned. - -- * **Waiting**: Waiting for an event. - -- * **Report**: Reporting. - -- * **Account**: Account for an event. - -- * **Accounted**: All events have been accounted for, end of the process. - -- * **Failed**: Failed the process. - -- - -- ### ACT_ACCOUNT Events - -- - -- * **Start**: Start the process. - -- * **Wait**: Wait for an event. - -- * **Report**: Report the status of the accounting. - -- * **Event**: An event happened, process the event. - -- * **More**: More targets. - -- * **NoMore (*)**: No more targets. - -- * **Fail (*)**: The action process has failed. - -- - -- (*) End states of the process. - -- - -- ### ACT_ACCOUNT state transition methods: - -- - -- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. - -- There are 2 moments when state transition methods will be called by the state machine: - -- - -- * **Before** the state transition. - -- The state transition method needs to start with the name **OnBefore + the name of the state**. - -- If the state transition method returns false, then the processing of the state transition will not be done! - -- If you want to change the behaviour of the AIControllable at this event, return false, - -- but then you'll need to specify your own logic using the AIControllable! - -- - -- * **After** the state transition. - -- The state transition method needs to start with the name **OnAfter + the name of the state**. - -- These state transition methods need to provide a return value, which is specified at the function description. - -- - -- @type ACT_ACCOUNT - -- @field Set#SET_UNIT TargetSetUnit - -- @extends Core.Fsm#FSM_PROCESS - ACT_ACCOUNT = { - ClassName = "ACT_ACCOUNT", - TargetSetUnit = nil, - } - - --- Creates a new DESTROY process. - -- @param #ACT_ACCOUNT self - -- @return #ACT_ACCOUNT - function ACT_ACCOUNT:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_PROCESS:New() ) -- Core.Fsm#FSM_PROCESS - - self:AddTransition( "Assigned", "Start", "Waiting" ) - self:AddTransition( "*", "Wait", "Waiting" ) - self:AddTransition( "*", "Report", "Report" ) - self:AddTransition( "*", "Event", "Account" ) - self:AddTransition( "Account", "Player", "AccountForPlayer" ) - self:AddTransition( "Account", "Other", "AccountForOther" ) - self:AddTransition( { "Account", "AccountForPlayer", "AccountForOther" }, "More", "Wait" ) - self:AddTransition( { "Account", "AccountForPlayer", "AccountForOther" }, "NoMore", "Accounted" ) - self:AddTransition( "*", "Fail", "Failed" ) - - self:AddEndState( "Failed" ) - - self:SetStartState( "Assigned" ) - - return self - end - - --- Process Events - - --- StateMachine callback function - -- @param #ACT_ACCOUNT self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT:onafterStart( ProcessUnit, From, Event, To ) - - self:HandleEvent( EVENTS.Dead, self.onfuncEventDead ) - self:HandleEvent( EVENTS.Crash, self.onfuncEventCrash ) - self:HandleEvent( EVENTS.Hit ) - - self:__Wait( 1 ) - end - - - --- StateMachine callback function - -- @param #ACT_ACCOUNT self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT:onenterWaiting( ProcessUnit, From, Event, To ) - - if self.DisplayCount >= self.DisplayInterval then - self:Report() - self.DisplayCount = 1 - else - self.DisplayCount = self.DisplayCount + 1 - end - - return true -- Process always the event. - end - - --- StateMachine callback function - -- @param #ACT_ACCOUNT self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT:onafterEvent( ProcessUnit, From, Event, To, Event ) - - self:__NoMore( 1 ) - end - -end -- ACT_ACCOUNT - -do -- ACT_ACCOUNT_DEADS - - --- # @{#ACT_ACCOUNT_DEADS} FSM class, extends @{Fsm.Account#ACT_ACCOUNT} - -- - -- The ACT_ACCOUNT_DEADS class accounts (detects, counts and reports) successful kills of DCS units. - -- The process is given a @{Set} of units that will be tracked upon successful destruction. - -- The process will end after each target has been successfully destroyed. - -- Each successful dead will trigger an Account state transition that can be scored, modified or administered. - -- - -- - -- ## ACT_ACCOUNT_DEADS constructor: - -- - -- * @{#ACT_ACCOUNT_DEADS.New}(): Creates a new ACT_ACCOUNT_DEADS object. - -- - -- @type ACT_ACCOUNT_DEADS - -- @field Set#SET_UNIT TargetSetUnit - -- @extends #ACT_ACCOUNT - ACT_ACCOUNT_DEADS = { - ClassName = "ACT_ACCOUNT_DEADS", - } - - - --- Creates a new DESTROY process. - -- @param #ACT_ACCOUNT_DEADS self - -- @param Set#SET_UNIT TargetSetUnit - -- @param #string TaskName - function ACT_ACCOUNT_DEADS:New() - -- Inherits from BASE - local self = BASE:Inherit( self, ACT_ACCOUNT:New() ) -- #ACT_ACCOUNT_DEADS - - self.DisplayInterval = 30 - self.DisplayCount = 30 - self.DisplayMessage = true - self.DisplayTime = 10 -- 10 seconds is the default - self.DisplayCategory = "HQ" -- Targets is the default display category - - return self - end - - function ACT_ACCOUNT_DEADS:Init( FsmAccount ) - - self.Task = self:GetTask() - self.TaskName = self.Task:GetName() - end - - --- Process Events - - --- StateMachine callback function - -- @param #ACT_ACCOUNT_DEADS self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT_DEADS:onenterReport( ProcessUnit, Task, From, Event, To ) - self:E( { ProcessUnit, From, Event, To } ) - - local MessageText = "Your group with assigned " .. self.TaskName .. " task has " .. Task.TargetSetUnit:GetUnitTypesText() .. " targets left to be destroyed." - self:GetCommandCenter():MessageTypeToGroup( MessageText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) - end - - - --- StateMachine callback function - -- @param #ACT_ACCOUNT_DEADS self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param Tasking.Task#TASK Task - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param Core.Event#EVENTDATA EventData - function ACT_ACCOUNT_DEADS:onafterEvent( ProcessUnit, Task, From, Event, To, EventData ) - self:T( { ProcessUnit:GetName(), Task:GetName(), From, Event, To, EventData } ) - - if Task.TargetSetUnit:FindUnit( EventData.IniUnitName ) then - local PlayerName = ProcessUnit:GetPlayerName() - local PlayerHit = self.PlayerHits and self.PlayerHits[EventData.IniUnitName] - if PlayerHit == PlayerName then - self:Player( EventData ) - else - self:Other( EventData ) - end - end - end - - --- StateMachine callback function - -- @param #ACT_ACCOUNT_DEADS self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param Tasking.Task#TASK Task - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param Core.Event#EVENTDATA EventData - function ACT_ACCOUNT_DEADS:onenterAccountForPlayer( ProcessUnit, Task, From, Event, To, EventData ) - self:T( { ProcessUnit:GetName(), Task:GetName(), From, Event, To, EventData } ) - - local TaskGroup = ProcessUnit:GetGroup() - - Task.TargetSetUnit:Remove( EventData.IniUnitName ) - - local MessageText = "You have destroyed a target.\nYour group assigned with task " .. self.TaskName .. " has\n" .. Task.TargetSetUnit:Count() .. " targets ( " .. Task.TargetSetUnit:GetUnitTypesText() .. " ) left to be destroyed." - self:GetCommandCenter():MessageTypeToGroup( MessageText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) - - local PlayerName = ProcessUnit:GetPlayerName() - Task:AddProgress( PlayerName, "Destroyed " .. EventData.IniTypeName, timer.getTime(), 1 ) - - if Task.TargetSetUnit:Count() > 0 then - self:__More( 1 ) - else - self:__NoMore( 1 ) - end - end - - --- StateMachine callback function - -- @param #ACT_ACCOUNT_DEADS self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param Tasking.Task#TASK Task - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param Core.Event#EVENTDATA EventData - function ACT_ACCOUNT_DEADS:onenterAccountForOther( ProcessUnit, Task, From, Event, To, EventData ) - self:T( { ProcessUnit:GetName(), Task:GetName(), From, Event, To, EventData } ) - - local TaskGroup = ProcessUnit:GetGroup() - Task.TargetSetUnit:Remove( EventData.IniUnitName ) - - local MessageText = "One of the task targets has been destroyed.\nYour group assigned with task " .. self.TaskName .. " has\n" .. Task.TargetSetUnit:Count() .. " targets ( " .. Task.TargetSetUnit:GetUnitTypesText() .. " ) left to be destroyed." - self:GetCommandCenter():MessageTypeToGroup( MessageText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) - - if Task.TargetSetUnit:Count() > 0 then - self:__More( 1 ) - else - self:__NoMore( 1 ) - end - end - - - --- DCS Events - - --- @param #ACT_ACCOUNT_DEADS self - -- @param Core.Event#EVENTDATA EventData - function ACT_ACCOUNT_DEADS:OnEventHit( EventData ) - self:T( { "EventDead", EventData } ) - - if EventData.IniPlayerName and EventData.TgtDCSUnitName then - self.PlayerHits = self.PlayerHits or {} - self.PlayerHits[EventData.TgtDCSUnitName] = EventData.IniPlayerName - end - end - - --- @param #ACT_ACCOUNT_DEADS self - -- @param Event#EVENTDATA EventData - function ACT_ACCOUNT_DEADS:onfuncEventDead( EventData ) - self:T( { "EventDead", EventData } ) - - if EventData.IniDCSUnit then - self:Event( EventData ) - end - end - - --- DCS Events - - --- @param #ACT_ACCOUNT_DEADS self - -- @param Event#EVENTDATA EventData - function ACT_ACCOUNT_DEADS:onfuncEventCrash( EventData ) - self:T( { "EventDead", EventData } ) - - if EventData.IniDCSUnit then - self:Event( EventData ) - end - end - -end -- ACT_ACCOUNT DEADS ---- (SP) (MP) (FSM) Route AI or players through waypoints or to zones. --- --- === --- --- # @{#ACT_ASSIST} FSM class, extends @{Fsm#FSM_PROCESS} --- --- ## ACT_ASSIST state machine: --- --- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. --- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, --- but will have **different implementation behaviour** upon each event or state transition. --- --- ### ACT_ASSIST **Events**: --- --- These are the events defined in this class: --- --- * **Start**: The process is started. --- * **Next**: The process is smoking the targets in the given zone. --- --- ### ACT_ASSIST **Event methods**: --- --- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. --- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: --- --- * **Immediate**: The event method has exactly the name of the event. --- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. --- --- ### ACT_ASSIST **States**: --- --- * **None**: The controllable did not receive route commands. --- * **AwaitSmoke (*)**: The process is awaiting to smoke the targets in the zone. --- * **Smoking (*)**: The process is smoking the targets in the zone. --- * **Failed (*)**: The process has failed. --- --- (*) End states of the process. --- --- ### ACT_ASSIST state transition methods: --- --- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. --- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Before** the state transition. --- The state transition method needs to start with the name **OnBefore + the name of the state**. --- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, --- but then you'll need to specify your own logic using the AIControllable! --- --- * **After** the state transition. --- The state transition method needs to start with the name **OnAfter + the name of the state**. --- These state transition methods need to provide a return value, which is specified at the function description. --- --- === --- --- # 1) @{#ACT_ASSIST_SMOKE_TARGETS_ZONE} class, extends @{Fsm.Route#ACT_ASSIST} --- --- The ACT_ASSIST_SMOKE_TARGETS_ZONE class implements the core functions to smoke targets in a @{Zone}. --- The targets are smoked within a certain range around each target, simulating a realistic smoking behaviour. --- At random intervals, a new target is smoked. --- --- # 1.1) ACT_ASSIST_SMOKE_TARGETS_ZONE constructor: --- --- * @{#ACT_ASSIST_SMOKE_TARGETS_ZONE.New}(): Creates a new ACT_ASSIST_SMOKE_TARGETS_ZONE object. --- --- === --- --- @module Smoke - -do -- ACT_ASSIST - - --- ACT_ASSIST class - -- @type ACT_ASSIST - -- @extends Core.Fsm#FSM_PROCESS - ACT_ASSIST = { - ClassName = "ACT_ASSIST", - } - - --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. - -- @param #ACT_ASSIST self - -- @return #ACT_ASSIST - function ACT_ASSIST:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ASSIST" ) ) -- Core.Fsm#FSM_PROCESS - - self:AddTransition( "None", "Start", "AwaitSmoke" ) - self:AddTransition( "AwaitSmoke", "Next", "Smoking" ) - self:AddTransition( "Smoking", "Next", "AwaitSmoke" ) - self:AddTransition( "*", "Stop", "Success" ) - self:AddTransition( "*", "Fail", "Failed" ) - - self:AddEndState( "Failed" ) - self:AddEndState( "Success" ) - - self:SetStartState( "None" ) - - return self - end - - --- Task Events - - --- StateMachine callback function - -- @param #ACT_ASSIST self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIST:onafterStart( ProcessUnit, From, Event, To ) - - local ProcessGroup = ProcessUnit:GetGroup() - local MissionMenu = self:GetMission():GetMenu( ProcessGroup ) - - local function MenuSmoke( MenuParam ) - self:E( MenuParam ) - local self = MenuParam.self - local SmokeColor = MenuParam.SmokeColor - self.SmokeColor = SmokeColor - self:__Next( 1 ) - end - - self.Menu = MENU_GROUP:New( ProcessGroup, "Target acquisition", MissionMenu ) - self.MenuSmokeBlue = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop blue smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Blue } ) - self.MenuSmokeGreen = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop green smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Green } ) - self.MenuSmokeOrange = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop Orange smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Orange } ) - self.MenuSmokeRed = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop Red smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Red } ) - self.MenuSmokeWhite = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop White smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.White } ) - end - - --- StateMachine callback function - -- @param #ACT_ASSIST self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIST:onafterStop( ProcessUnit, From, Event, To ) - - self.Menu:Remove() -- When stopped, remove the menus - end - -end - -do -- ACT_ASSIST_SMOKE_TARGETS_ZONE - - --- ACT_ASSIST_SMOKE_TARGETS_ZONE class - -- @type ACT_ASSIST_SMOKE_TARGETS_ZONE - -- @field Set#SET_UNIT TargetSetUnit - -- @field Core.Zone#ZONE_BASE TargetZone - -- @extends #ACT_ASSIST - ACT_ASSIST_SMOKE_TARGETS_ZONE = { - ClassName = "ACT_ASSIST_SMOKE_TARGETS_ZONE", - } - --- function ACT_ASSIST_SMOKE_TARGETS_ZONE:_Destructor() --- self:E("_Destructor") --- --- self.Menu:Remove() --- self:EventRemoveAll() --- end - - --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. - -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self - -- @param Set#SET_UNIT TargetSetUnit - -- @param Core.Zone#ZONE_BASE TargetZone - function ACT_ASSIST_SMOKE_TARGETS_ZONE:New( TargetSetUnit, TargetZone ) - local self = BASE:Inherit( self, ACT_ASSIST:New() ) -- #ACT_ASSIST - - self.TargetSetUnit = TargetSetUnit - self.TargetZone = TargetZone - - return self - end - - function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init( FsmSmoke ) - - self.TargetSetUnit = FsmSmoke.TargetSetUnit - self.TargetZone = FsmSmoke.TargetZone - end - - --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. - -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self - -- @param Set#SET_UNIT TargetSetUnit - -- @param Core.Zone#ZONE_BASE TargetZone - -- @return #ACT_ASSIST_SMOKE_TARGETS_ZONE self - function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init( TargetSetUnit, TargetZone ) - - self.TargetSetUnit = TargetSetUnit - self.TargetZone = TargetZone - - return self - end - - --- StateMachine callback function - -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIST_SMOKE_TARGETS_ZONE:onenterSmoking( ProcessUnit, From, Event, To ) - - self.TargetSetUnit:ForEachUnit( - --- @param Wrapper.Unit#UNIT SmokeUnit - function( SmokeUnit ) - if math.random( 1, ( 100 * self.TargetSetUnit:Count() ) / 4 ) <= 100 then - SCHEDULER:New( self, - function() - if SmokeUnit:IsAlive() then - SmokeUnit:Smoke( self.SmokeColor, 150 ) - end - end, {}, math.random( 10, 60 ) - ) - end - end - ) - - end - -end--- **Tasking** -- A COMMANDCENTER is the owner of multiple missions within MOOSE. --- A COMMANDCENTER governs multiple missions, the tasking and the reporting. --- --- === --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- ==== --- --- @module CommandCenter - - - - - ---- The COMMANDCENTER class --- @type COMMANDCENTER --- @field Wrapper.Group#GROUP HQ --- @field Dcs.DCSCoalitionWrapper.Object#coalition CommandCenterCoalition --- @list Missions --- @extends Core.Base#BASE - - ---- # COMMANDCENTER class, extends @{Base#BASE} --- --- The COMMANDCENTER class governs multiple missions, the tasking and the reporting. --- --- The commandcenter communicates important messages between the various groups of human players executing tasks in missions. --- --- ## COMMANDCENTER constructor --- --- * @{#COMMANDCENTER.New}(): Creates a new COMMANDCENTER object. --- --- ## Mission Management --- --- * @{#COMMANDCENTER.AddMission}(): Adds a mission to the commandcenter control. --- * @{#COMMANDCENTER.RemoveMission}(): Removes a mission to the commandcenter control. --- * @{#COMMANDCENTER.GetMissions}(): Retrieves the missions table controlled by the commandcenter. --- --- ## Reference Zones --- --- Command Centers may be aware of certain Reference Zones within the battleground. These Reference Zones can refer to --- known areas, recognizable buildings or sites, or any other point of interest. --- Command Centers will use these Reference Zones to help pilots with defining coordinates in terms of navigation --- during the WWII era. --- The Reference Zones are related to the WWII mode that the Command Center will operate in. --- Use the method @{#COMMANDCENTER.SetModeWWII}() to set the mode of communication to the WWII mode. --- --- In WWII mode, the Command Center will receive detected targets, and will select for each target the closest --- nearby Reference Zone. This allows pilots to navigate easier through the battle field readying for combat. --- --- The Reference Zones need to be set by the Mission Designer in the Mission Editor. --- Reference Zones are set by normal trigger zones. One can color the zones in a specific color, --- and the radius of the zones doesn't matter, only the point is important. Place the center of these Reference Zones at --- specific scenery objects or points of interest (like cities, rivers, hills, crossing etc). --- The trigger zones indicating a Reference Zone need to follow a specific syntax. --- The name of each trigger zone expressing a Reference Zone need to start with a classification name of the object, --- followed by a #, followed by a symbolic name of the Reference Zone. --- A few examples: --- --- * A church at Tskinvali would be indicated as: *Church#Tskinvali* --- * A train station near Kobuleti would be indicated as: *Station#Kobuleti* --- --- The COMMANDCENTER class contains a method to indicate which trigger zones need to be used as Reference Zones. --- This is done by using the method @{#COMMANDCENTER.SetReferenceZones}(). --- For the moment, only one Reference Zone class can be specified, but in the future, more classes will become possible. --- --- @field #COMMANDCENTER -COMMANDCENTER = { - ClassName = "COMMANDCENTER", - CommandCenterName = "", - CommandCenterCoalition = nil, - CommandCenterPositionable = nil, - Name = "", - ReferencePoints = {}, - ReferenceNames = {}, - CommunicationMode = "80", -} - ---- The constructor takes an IDENTIFIABLE as the HQ command center. --- @param #COMMANDCENTER self --- @param Wrapper.Positionable#POSITIONABLE CommandCenterPositionable --- @param #string CommandCenterName --- @return #COMMANDCENTER -function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) - - local self = BASE:Inherit( self, BASE:New() ) - - self.CommandCenterPositionable = CommandCenterPositionable - self.CommandCenterName = CommandCenterName or CommandCenterPositionable:GetName() - self.CommandCenterCoalition = CommandCenterPositionable:GetCoalition() - - self.Missions = {} - - self:HandleEvent( EVENTS.Birth, - --- @param #COMMANDCENTER self - -- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - if EventData.IniObjectCategory == 1 then - local EventGroup = GROUP:Find( EventData.IniDCSGroup ) - if EventGroup and self:HasGroup( EventGroup ) then - local MenuReporting = MENU_GROUP:New( EventGroup, "Missions Reports", self.CommandCenterMenu ) - local MenuMissionsSummary = MENU_GROUP_COMMAND:New( EventGroup, "Missions Status Report", MenuReporting, self.ReportMissionsStatus, self, EventGroup ) - local MenuMissionsDetails = MENU_GROUP_COMMAND:New( EventGroup, "Missions Players Report", MenuReporting, self.ReportMissionsPlayers, self, EventGroup ) - self:ReportSummary( EventGroup ) - local PlayerUnit = EventData.IniUnit - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled! - Mission:JoinUnit( PlayerUnit, PlayerGroup ) - end - self:SetMenu() - _DATABASE:PlayerSettingsMenu( PlayerUnit ) - end - end - - end - ) - - -- When a player enters a client or a unit, the CommandCenter will check for each Mission and each Task in the Mission if the player has things to do. - -- For these elements, it will= - -- - Set the correct menu. - -- - Assign the PlayerUnit to the Task if required. - -- - Send a message to the other players in the group that this player has joined. - self:HandleEvent( EVENTS.PlayerEnterUnit, - --- @param #COMMANDCENTER self - -- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - local PlayerUnit = EventData.IniUnit - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled! - Mission:JoinUnit( PlayerUnit, PlayerGroup ) - end - self:SetMenu() - end - ) - - -- Handle when a player leaves a slot and goes back to spectators ... - -- The PlayerUnit will be UnAssigned from the Task. - -- When there is no Unit left running the Task, the Task goes into Abort... - self:HandleEvent( EVENTS.MissionEnd, - --- @param #TASK self - -- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - local PlayerUnit = EventData.IniUnit - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - Mission:Stop() - end - end - ) - - -- Handle when a player leaves a slot and goes back to spectators ... - -- The PlayerUnit will be UnAssigned from the Task. - -- When there is no Unit left running the Task, the Task goes into Abort... - self:HandleEvent( EVENTS.PlayerLeaveUnit, - --- @param #TASK self - -- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - local PlayerUnit = EventData.IniUnit - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - if Mission:IsENGAGED() then - Mission:AbortUnit( PlayerUnit ) - end - end - end - ) - - -- Handle when a player leaves a slot and goes back to spectators ... - -- The PlayerUnit will be UnAssigned from the Task. - -- When there is no Unit left running the Task, the Task goes into Abort... - self:HandleEvent( EVENTS.Crash, - --- @param #TASK self - -- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - local PlayerUnit = EventData.IniUnit - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - if Mission:IsENGAGED() then - Mission:CrashUnit( PlayerUnit ) - end - end - end - ) - - self:SetMenu() - - _SETTINGS:SetSystemMenu( CommandCenterPositionable ) - - return self -end - ---- Gets the name of the HQ command center. --- @param #COMMANDCENTER self --- @return #string -function COMMANDCENTER:GetName() - - return self.CommandCenterName -end - ---- Gets the POSITIONABLE of the HQ command center. --- @param #COMMANDCENTER self --- @return Wrapper.Positionable#POSITIONABLE -function COMMANDCENTER:GetPositionable() - return self.CommandCenterPositionable -end - ---- Get the Missions governed by the HQ command center. --- @param #COMMANDCENTER self --- @return #list -function COMMANDCENTER:GetMissions() - - return self.Missions -end - ---- Add a MISSION to be governed by the HQ command center. --- @param #COMMANDCENTER self --- @param Tasking.Mission#MISSION Mission --- @return Tasking.Mission#MISSION -function COMMANDCENTER:AddMission( Mission ) - - self.Missions[Mission] = Mission - - return Mission -end - ---- Removes a MISSION to be governed by the HQ command center. --- The given Mission is not nilified. --- @param #COMMANDCENTER self --- @param Tasking.Mission#MISSION Mission --- @return Tasking.Mission#MISSION -function COMMANDCENTER:RemoveMission( Mission ) - - self.Missions[Mission] = nil - - return Mission -end - ---- Set special Reference Zones known by the Command Center to guide airborne pilots during WWII. --- --- These Reference Zones are normal trigger zones, with a special naming. --- The Reference Zones need to be set by the Mission Designer in the Mission Editor. --- Reference Zones are set by normal trigger zones. One can color the zones in a specific color, --- and the radius of the zones doesn't matter, only the center of the zone is important. Place the center of these Reference Zones at --- specific scenery objects or points of interest (like cities, rivers, hills, crossing etc). --- The trigger zones indicating a Reference Zone need to follow a specific syntax. --- The name of each trigger zone expressing a Reference Zone need to start with a classification name of the object, --- followed by a #, followed by a symbolic name of the Reference Zone. --- A few examples: --- --- * A church at Tskinvali would be indicated as: *Church#Tskinvali* --- * A train station near Kobuleti would be indicated as: *Station#Kobuleti* --- --- Taking the above example, this is how this method would be used: --- --- CC:SetReferenceZones( "Church" ) --- CC:SetReferenceZones( "Station" ) --- --- --- @param #COMMANDCENTER self --- @param #string ReferenceZonePrefix The name before the #-mark indicating the class of the Reference Zones. --- @return #COMMANDCENTER -function COMMANDCENTER:SetReferenceZones( ReferenceZonePrefix ) - local MatchPattern = "(.*)#(.*)" - self:F( { MatchPattern = MatchPattern } ) - for ReferenceZoneName in pairs( _DATABASE.ZONENAMES ) do - local ZoneName, ReferenceName = string.match( ReferenceZoneName, MatchPattern ) - self:F( { ZoneName = ZoneName, ReferenceName = ReferenceName } ) - if ZoneName and ReferenceName and ZoneName == ReferenceZonePrefix then - self.ReferencePoints[ReferenceZoneName] = ZONE:New( ReferenceZoneName ) - self.ReferenceNames[ReferenceZoneName] = ReferenceName - end - end - return self -end - ---- Set the commandcenter operations in WWII mode --- This will disable LL, MGRS, BRA, BULLS navigatin messages sent by the Command Center, --- and will be replaced by a navigation using Reference Zones. --- It will also disable the settings at the settings menu for these. --- @param #COMMANDCENTER self --- @return #COMMANDCENTER -function COMMANDCENTER:SetModeWWII() - self.CommunicationMode = "WWII" - return self -end - - ---- Returns if the commandcenter operations is in WWII mode --- @param #COMMANDCENTER self --- @return #boolean true if in WWII mode. -function COMMANDCENTER:IsModeWWII() - return self.CommunicationMode == "WWII" -end - - - - ---- Sets the menu structure of the Missions governed by the HQ command center. --- @param #COMMANDCENTER self -function COMMANDCENTER:SetMenu() - self:F() - - self.CommandCenterMenu = self.CommandCenterMenu or MENU_COALITION:New( self.CommandCenterCoalition, "Command Center (" .. self:GetName() .. ")" ) - - local MenuTime = timer.getTime() - for MissionID, Mission in pairs( self:GetMissions() or {} ) do - local Mission = Mission -- Tasking.Mission#MISSION - Mission:SetMenu( MenuTime ) - end - - for MissionID, Mission in pairs( self:GetMissions() or {} ) do - local Mission = Mission -- Tasking.Mission#MISSION - Mission:RemoveMenu( MenuTime ) - end - -end - ---- Gets the commandcenter menu structure governed by the HQ command center. --- @param #COMMANDCENTER self --- @return Core.Menu#MENU_COALITION -function COMMANDCENTER:GetMenu() - return self.CommandCenterMenu -end - ---- Checks of the COMMANDCENTER has a GROUP. --- @param #COMMANDCENTER self --- @param Wrapper.Group#GROUP --- @return #boolean -function COMMANDCENTER:HasGroup( MissionGroup ) - - local Has = false - - for MissionID, Mission in pairs( self.Missions ) do - local Mission = Mission -- Tasking.Mission#MISSION - if Mission:HasGroup( MissionGroup ) then - Has = true - break - end - end - - return Has -end - ---- Send a CC message to the coalition of the CC. --- @param #COMMANDCENTER self -function COMMANDCENTER:MessageToAll( Message ) - - self:GetPositionable():MessageToAll( Message, 20, self:GetName() ) - -end - ---- Send a CC message to a GROUP. --- @param #COMMANDCENTER self --- @param #string Message --- @param Wrapper.Group#GROUP TaskGroup -function COMMANDCENTER:MessageToGroup( Message, TaskGroup ) - - self:GetPositionable():MessageToGroup( Message, 15, TaskGroup, self:GetName() ) - -end - ---- Send a CC message of a specified type to a GROUP. --- @param #COMMANDCENTER self --- @param #string Message --- @param Wrapper.Group#GROUP TaskGroup --- @param Core.Message#MESSAGE.MessageType MessageType The type of the message, resulting in automatic time duration and prefix of the message. -function COMMANDCENTER:MessageTypeToGroup( Message, TaskGroup, MessageType ) - - self:GetPositionable():MessageTypeToGroup( Message, MessageType, TaskGroup, self:GetName() ) - -end - ---- Send a CC message to the coalition of the CC. --- @param #COMMANDCENTER self -function COMMANDCENTER:MessageToCoalition( Message ) - - local CCCoalition = self:GetPositionable():GetCoalition() - --TODO: Fix coalition bug! - - self:GetPositionable():MessageToCoalition( Message, 15, CCCoalition ) - -end - - ---- Send a CC message of a specified type to the coalition of the CC. --- @param #COMMANDCENTER self --- @param #string Message The message. --- @param Core.Message#MESSAGE.MessageType MessageType The type of the message, resulting in automatic time duration and prefix of the message. -function COMMANDCENTER:MessageTypeToCoalition( Message, MessageType ) - - local CCCoalition = self:GetPositionable():GetCoalition() - --TODO: Fix coalition bug! - - self:GetPositionable():MessageTypeToCoalition( Message, MessageType, CCCoalition ) - -end - - ---- Report the status of all MISSIONs to a GROUP. --- Each Mission is listed, with an indication how many Tasks are still to be completed. --- @param #COMMANDCENTER self -function COMMANDCENTER:ReportMissionsStatus( ReportGroup ) - self:E( ReportGroup ) - - local Report = REPORT:New() - - Report:Add( "Status report of all missions." ) - - for MissionID, Mission in pairs( self.Missions ) do - local Mission = Mission -- Tasking.Mission#MISSION - Report:Add( " - " .. Mission:ReportStatus() ) - end - - self:MessageToGroup( Report:Text(), ReportGroup ) -end - ---- Report the players of all MISSIONs to a GROUP. --- Each Mission is listed, with an indication how many Tasks are still to be completed. --- @param #COMMANDCENTER self -function COMMANDCENTER:ReportMissionsPlayers( ReportGroup ) - self:E( ReportGroup ) - - local Report = REPORT:New() - - Report:Add( "Players active in all missions." ) - - for MissionID, Mission in pairs( self.Missions ) do - local Mission = Mission -- Tasking.Mission#MISSION - Report:Add( " - " .. Mission:ReportPlayers() ) - end - - self:MessageToGroup( Report:Text(), ReportGroup ) -end - ---- Report the status of a Task to a Group. --- Report the details of a Mission, listing the Mission, and all the Task details. --- @param #COMMANDCENTER self -function COMMANDCENTER:ReportDetails( ReportGroup, Task ) - self:E( ReportGroup ) - - local Report = REPORT:New() - - for MissionID, Mission in pairs( self.Missions ) do - local Mission = Mission -- Tasking.Mission#MISSION - Report:Add( " - " .. Mission:ReportDetails() ) - end - - self:MessageToGroup( Report:Text(), ReportGroup ) -end - ---- **Tasking** -- A MISSION is the main owner of a Mission orchestration within MOOSE. --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- === --- --- @module Mission - ---- The MISSION class --- @type MISSION --- @field #MISSION.Clients _Clients --- @field Core.Menu#MENU_COALITION MissionMenu --- @field #string MissionBriefing --- @extends Core.Fsm#FSM -MISSION = { - ClassName = "MISSION", - Name = "", - MissionStatus = "PENDING", - AssignedGroups = {}, -} - ---- This is the main MISSION declaration method. Each Mission is like the master or a Mission orchestration between, Clients, Tasks, Stages etc. --- @param #MISSION self --- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter --- @param #string MissionName is the name of the mission. This name will be used to reference the status of each mission by the players. --- @param #string MissionPriority is a string indicating the "priority" of the Mission. f.e. "Primary", "Secondary" or "First", "Second". It is free format and up to the Mission designer to choose. There are no rules behind this field. --- @param #string MissionBriefing is a string indicating the mission briefing to be shown when a player joins a @{CLIENT}. --- @param Dcs.DCSCoalitionWrapper.Object#coalition MissionCoalition is a string indicating the coalition or party to which this mission belongs to. It is free format and can be chosen freely by the mission designer. Note that this field is not to be confused with the coalition concept of the ME. Examples of a Mission Coalition could be "NATO", "CCCP", "Intruders", "Terrorists"... --- @return #MISSION self -function MISSION:New( CommandCenter, MissionName, MissionPriority, MissionBriefing, MissionCoalition ) - - local self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM - - self:T( { MissionName, MissionPriority, MissionBriefing, MissionCoalition } ) - - self.CommandCenter = CommandCenter - CommandCenter:AddMission( self ) - - self.Name = MissionName - self.MissionPriority = MissionPriority - self.MissionBriefing = MissionBriefing - self.MissionCoalition = MissionCoalition - - self.Tasks = {} - self.PlayerNames = {} -- These are the players that achieved progress in the mission. - - self:SetStartState( "IDLE" ) - - self:AddTransition( "IDLE", "Start", "ENGAGED" ) - - --- OnLeave Transition Handler for State IDLE. - -- @function [parent=#MISSION] OnLeaveIDLE - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnEnter Transition Handler for State IDLE. - -- @function [parent=#MISSION] OnEnterIDLE - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- OnLeave Transition Handler for State ENGAGED. - -- @function [parent=#MISSION] OnLeaveENGAGED - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnEnter Transition Handler for State ENGAGED. - -- @function [parent=#MISSION] OnEnterENGAGED - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- OnBefore Transition Handler for Event Start. - -- @function [parent=#MISSION] OnBeforeStart - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Start. - -- @function [parent=#MISSION] OnAfterStart - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Start. - -- @function [parent=#MISSION] Start - -- @param #MISSION self - - --- Asynchronous Event Trigger for Event Start. - -- @function [parent=#MISSION] __Start - -- @param #MISSION self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "ENGAGED", "Stop", "IDLE" ) - - --- OnLeave Transition Handler for State IDLE. - -- @function [parent=#MISSION] OnLeaveIDLE - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnEnter Transition Handler for State IDLE. - -- @function [parent=#MISSION] OnEnterIDLE - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- OnBefore Transition Handler for Event Stop. - -- @function [parent=#MISSION] OnBeforeStop - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Stop. - -- @function [parent=#MISSION] OnAfterStop - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Stop. - -- @function [parent=#MISSION] Stop - -- @param #MISSION self - - --- Asynchronous Event Trigger for Event Stop. - -- @function [parent=#MISSION] __Stop - -- @param #MISSION self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "ENGAGED", "Complete", "COMPLETED" ) - - --- OnLeave Transition Handler for State COMPLETED. - -- @function [parent=#MISSION] OnLeaveCOMPLETED - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnEnter Transition Handler for State COMPLETED. - -- @function [parent=#MISSION] OnEnterCOMPLETED - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- OnBefore Transition Handler for Event Complete. - -- @function [parent=#MISSION] OnBeforeComplete - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Complete. - -- @function [parent=#MISSION] OnAfterComplete - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Complete. - -- @function [parent=#MISSION] Complete - -- @param #MISSION self - - --- Asynchronous Event Trigger for Event Complete. - -- @function [parent=#MISSION] __Complete - -- @param #MISSION self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Fail", "FAILED" ) - - --- OnLeave Transition Handler for State FAILED. - -- @function [parent=#MISSION] OnLeaveFAILED - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnEnter Transition Handler for State FAILED. - -- @function [parent=#MISSION] OnEnterFAILED - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- OnBefore Transition Handler for Event Fail. - -- @function [parent=#MISSION] OnBeforeFail - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Fail. - -- @function [parent=#MISSION] OnAfterFail - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Fail. - -- @function [parent=#MISSION] Fail - -- @param #MISSION self - - --- Asynchronous Event Trigger for Event Fail. - -- @function [parent=#MISSION] __Fail - -- @param #MISSION self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "*", "MissionGoals", "*" ) - - --- MissionGoals Handler OnBefore for MISSION - -- @function [parent=#MISSION] OnBeforeMissionGoals - -- @param #MISSION self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- MissionGoals Handler OnAfter for MISSION - -- @function [parent=#MISSION] OnAfterMissionGoals - -- @param #MISSION self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- MissionGoals Trigger for MISSION - -- @function [parent=#MISSION] MissionGoals - -- @param #MISSION self - - --- MissionGoals Asynchronous Trigger for MISSION - -- @function [parent=#MISSION] __MissionGoals - -- @param #MISSION self - -- @param #number Delay - - -- Private implementations - - CommandCenter:SetMenu() - - return self -end - - ---- FSM function for a MISSION --- @param #MISSION self --- @param #string From --- @param #string Event --- @param #string To -function MISSION:onenterCOMPLETED( From, Event, To ) - - self:GetCommandCenter():MessageTypeToCoalition( self:GetName() .. " has been completed! Good job guys!", MESSAGE.Type.Information ) -end - ---- Gets the mission name. --- @param #MISSION self --- @return #MISSION self -function MISSION:GetName() - return string.format( 'Mission "%s (%s)"', self.Name, self.MissionPriority ) -end - ---- Add a Unit to join the Mission. --- For each Task within the Mission, the Unit is joined with the Task. --- If the Unit was not part of a Task in the Mission, false is returned. --- If the Unit is part of a Task in the Mission, true is returned. --- @param #MISSION self --- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission. --- @param Wrapper.Group#GROUP PlayerGroup The GROUP of the player joining the Mission. --- @return #boolean true if Unit is part of a Task in the Mission. -function MISSION:JoinUnit( PlayerUnit, PlayerGroup ) - self:F( { PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } ) - - local PlayerUnitAdded = false - - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - if Task:JoinUnit( PlayerUnit, PlayerGroup ) then - PlayerUnitAdded = true - end - end - - return PlayerUnitAdded -end - ---- Aborts a PlayerUnit from the Mission. --- For each Task within the Mission, the PlayerUnit is removed from Task where it is assigned. --- If the Unit was not part of a Task in the Mission, false is returned. --- If the Unit is part of a Task in the Mission, true is returned. --- @param #MISSION self --- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission. --- @return #MISSION -function MISSION:AbortUnit( PlayerUnit ) - self:F( { PlayerUnit = PlayerUnit } ) - - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - local PlayerGroup = PlayerUnit:GetGroup() - Task:AbortGroup( PlayerGroup ) - end - - return self -end - ---- Handles a crash of a PlayerUnit from the Mission. --- For each Task within the Mission, the PlayerUnit is removed from Task where it is assigned. --- If the Unit was not part of a Task in the Mission, false is returned. --- If the Unit is part of a Task in the Mission, true is returned. --- @param #MISSION self --- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player crashing. --- @return #MISSION -function MISSION:CrashUnit( PlayerUnit ) - self:F( { PlayerUnit = PlayerUnit } ) - - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - local PlayerGroup = PlayerUnit:GetGroup() - Task:CrashGroup( PlayerGroup ) - end - - return self -end - ---- Add a scoring to the mission. --- @param #MISSION self --- @return #MISSION self -function MISSION:AddScoring( Scoring ) - self.Scoring = Scoring - return self -end - ---- Get the scoring object of a mission. --- @param #MISSION self --- @return #SCORING Scoring -function MISSION:GetScoring() - return self.Scoring -end - ---- Get the groups for which TASKS are given in the mission --- @param #MISSION self --- @return Core.Set#SET_GROUP -function MISSION:GetGroups() - - local SetGroup = SET_GROUP:New() - - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - local GroupSet = Task:GetGroups() - GroupSet:ForEachGroup( - function( TaskGroup ) - SetGroup:Add( TaskGroup, TaskGroup ) - end - ) - end - - return SetGroup - -end - - ---- Sets the Planned Task menu. --- @param #MISSION self --- @param #number MenuTime -function MISSION:SetMenu( MenuTime ) - self:F( { self:GetName(), MenuTime } ) - - for _, TaskData in pairs( self:GetTasks() ) do - local Task = TaskData -- Tasking.Task#TASK - Task:SetMenu( MenuTime ) - end -end - ---- Removes the Planned Task menu. --- @param #MISSION self --- @param #number MenuTime -function MISSION:RemoveMenu( MenuTime ) - self:F( { self:GetName(), MenuTime } ) - - for _, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - Task:RemoveMenu( MenuTime ) - end -end - - - -do -- Group Assignment - - --- Returns if the @{Mission} is assigned to the Group. - -- @param #MISSION self - -- @param Wrapper.Group#GROUP MissionGroup - -- @return #boolean - function MISSION:IsGroupAssigned( MissionGroup ) - - local MissionGroupName = MissionGroup:GetName() - - if self.AssignedGroups[MissionGroupName] == MissionGroup then - self:T( { "Mission is assigned to:", MissionGroup:GetName() } ) - return true - end - - self:T( { "Mission is not assigned to:", MissionGroup:GetName() } ) - return false - end - - - --- Set @{Group} assigned to the @{Mission}. - -- @param #MISSION self - -- @param Wrapper.Group#GROUP MissionGroup - -- @return #MISSION - function MISSION:SetGroupAssigned( MissionGroup ) - - local MissionName = self:GetName() - local MissionGroupName = MissionGroup:GetName() - - self.AssignedGroups[MissionGroupName] = MissionGroup - self:E( string.format( "Mission %s is assigned to %s", MissionName, MissionGroupName ) ) - - return self - end - - --- Clear the @{Group} assignment from the @{Mission}. - -- @param #MISSION self - -- @param Wrapper.Group#GROUP MissionGroup - -- @return #MISSION - function MISSION:ClearGroupAssignment( MissionGroup ) - - local MissionName = self:GetName() - local MissionGroupName = MissionGroup:GetName() - - self.AssignedGroups[MissionGroupName] = nil - --self:E( string.format( "Mission %s is unassigned to %s", MissionName, MissionGroupName ) ) - - return self - end - -end - ---- Gets the COMMANDCENTER. --- @param #MISSION self --- @return Tasking.CommandCenter#COMMANDCENTER -function MISSION:GetCommandCenter() - return self.CommandCenter -end - - ---- Removes a Task menu. --- @param #MISSION self --- @param Tasking.Task#TASK Task --- @return #MISSION self -function MISSION:RemoveTaskMenu( Task ) - - Task:RemoveMenu() -end - - ---- Gets the root mission menu for the TaskGroup. --- @param #MISSION self --- @return Core.Menu#MENU_COALITION self -function MISSION:GetRootMenu( TaskGroup ) -- R2.2 - - local CommandCenter = self:GetCommandCenter() - local CommandCenterMenu = CommandCenter:GetMenu() - - local MissionName = self:GetName() - --local MissionMenu = CommandCenterMenu:GetMenu( MissionName ) - - self.MissionMenu = self.MissionMenu or MENU_COALITION:New( self.MissionCoalition, self:GetName(), CommandCenterMenu ) - - return self.MissionMenu -end - ---- Gets the mission menu for the TaskGroup. --- @param #MISSION self --- @return Core.Menu#MENU_COALITION self -function MISSION:GetMenu( TaskGroup ) -- R2.1 -- Changed Menu Structure - - local CommandCenter = self:GetCommandCenter() - local CommandCenterMenu = CommandCenter:GetMenu() - - local MissionName = self:GetName() - --local MissionMenu = CommandCenterMenu:GetMenu( MissionName ) - - self.MissionGroupMenu = self.MissionGroupMenu or {} - self.MissionGroupMenu[TaskGroup] = self.MissionGroupMenu[TaskGroup] or {} - - local GroupMenu = self.MissionGroupMenu[TaskGroup] - - self.MissionMenu = self.MissionMenu or MENU_COALITION:New( self.MissionCoalition, self:GetName(), CommandCenterMenu ) - - GroupMenu.BriefingMenu = GroupMenu.BriefingMenu or MENU_GROUP_COMMAND:New( TaskGroup, "Mission Briefing", self.MissionMenu, self.MenuReportBriefing, self, TaskGroup ) - - GroupMenu.TaskReportsMenu = GroupMenu.TaskReportsMenu or MENU_GROUP:New( TaskGroup, "Task Reports", self.MissionMenu ) - GroupMenu.ReportTasksMenu = GroupMenu.ReportTasksMenu or MENU_GROUP_COMMAND:New( TaskGroup, "Report Tasks", GroupMenu.TaskReportsMenu, self.MenuReportTasksSummary, self, TaskGroup ) - GroupMenu.ReportPlannedTasksMenu = GroupMenu.ReportPlannedTasksMenu or MENU_GROUP_COMMAND:New( TaskGroup, "Report Planned Tasks", GroupMenu.TaskReportsMenu, self.MenuReportTasksPerStatus, self, TaskGroup, "Planned" ) - GroupMenu.ReportAssignedTasksMenu = GroupMenu.ReportAssignedTasksMenu or MENU_GROUP_COMMAND:New( TaskGroup, "Report Assigned Tasks", GroupMenu.TaskReportsMenu, self.MenuReportTasksPerStatus, self, TaskGroup, "Assigned" ) - GroupMenu.ReportSuccessTasksMenu = GroupMenu.ReportSuccessTasksMenu or MENU_GROUP_COMMAND:New( TaskGroup, "Report Successful Tasks", GroupMenu.TaskReportsMenu, self.MenuReportTasksPerStatus, self, TaskGroup, "Success" ) - GroupMenu.ReportFailedTasksMenu = GroupMenu.ReportFailedTasksMenu or MENU_GROUP_COMMAND:New( TaskGroup, "Report Failed Tasks", GroupMenu.TaskReportsMenu, self.MenuReportTasksPerStatus, self, TaskGroup, "Failed" ) - GroupMenu.ReportHeldTasksMenu = GroupMenu.ReportHeldTasksMenu or MENU_GROUP_COMMAND:New( TaskGroup, "Report Held Tasks", GroupMenu.TaskReportsMenu, self.MenuReportTasksPerStatus, self, TaskGroup, "Hold" ) - - GroupMenu.PlayerReportsMenu = GroupMenu.PlayerReportsMenu or MENU_GROUP:New( TaskGroup, "Statistics Reports", self.MissionMenu ) - GroupMenu.ReportMissionHistory = GroupMenu.ReportPlayersHistory or MENU_GROUP_COMMAND:New( TaskGroup, "Report Mission Progress", GroupMenu.PlayerReportsMenu, self.MenuReportPlayersProgress, self, TaskGroup ) - GroupMenu.ReportPlayersPerTaskMenu = GroupMenu.ReportPlayersPerTaskMenu or MENU_GROUP_COMMAND:New( TaskGroup, "Report Players per Task", GroupMenu.PlayerReportsMenu, self.MenuReportPlayersPerTask, self, TaskGroup ) - - return self.MissionMenu -end - - - - ---- Get the TASK identified by the TaskNumber from the Mission. This function is useful in GoalFunctions. --- @param #string TaskName The Name of the @{Task} within the @{Mission}. --- @return Tasking.Task#TASK The Task --- @return #nil Returns nil if no task was found. -function MISSION:GetTask( TaskName ) - self:F( { TaskName } ) - - return self.Tasks[TaskName] -end - - ---- Register a @{Task} to be completed within the @{Mission}. --- Note that there can be multiple @{Task}s registered to be completed. --- Each Task can be set a certain Goals. The Mission will not be completed until all Goals are reached. --- @param #MISSION self --- @param Tasking.Task#TASK Task is the @{Task} object. --- @return Tasking.Task#TASK The task added. -function MISSION:AddTask( Task ) - - local TaskName = Task:GetTaskName() - self:F( TaskName ) - - self.Tasks[TaskName] = self.Tasks[TaskName] or { n = 0 } - - self.Tasks[TaskName] = Task - - self:GetCommandCenter():SetMenu() - - return Task -end - ---- Removes a @{Task} to be completed within the @{Mission}. --- Note that there can be multiple @{Task}s registered to be completed. --- Each Task can be set a certain Goals. The Mission will not be completed until all Goals are reached. --- @param #MISSION self --- @param Tasking.Task#TASK Task is the @{Task} object. --- @return #nil The cleaned Task reference. -function MISSION:RemoveTask( Task ) - - local TaskName = Task:GetTaskName() - - self:F( TaskName ) - self.Tasks[TaskName] = self.Tasks[TaskName] or { n = 0 } - - -- Ensure everything gets garbarge collected. - self.Tasks[TaskName] = nil - Task = nil - - collectgarbage() - - self:GetCommandCenter():SetMenu() - - return nil -end - ---- Return the next @{Task} ID to be completed within the @{Mission}. --- @param #MISSION self --- @param Tasking.Task#TASK Task is the @{Task} object. --- @return Tasking.Task#TASK The task added. -function MISSION:GetNextTaskID( Task ) - - local TaskName = Task:GetTaskName() - self:F( TaskName ) - self.Tasks[TaskName] = self.Tasks[TaskName] or { n = 0 } - - self.Tasks[TaskName].n = self.Tasks[TaskName].n + 1 - - return self.Tasks[TaskName].n -end - ---- Is the @{Mission} **COMPLETED**. --- @param #MISSION self --- @return #boolean -function MISSION:IsCOMPLETED() - return self:Is( "COMPLETED" ) -end - ---- Is the @{Mission} **IDLE**. --- @param #MISSION self --- @return #boolean -function MISSION:IsIDLE() - return self:Is( "IDLE" ) -end - ---- Is the @{Mission} **ENGAGED**. --- @param #MISSION self --- @return #boolean -function MISSION:IsENGAGED() - return self:Is( "ENGAGED" ) -end - ---- Is the @{Mission} **FAILED**. --- @param #MISSION self --- @return #boolean -function MISSION:IsFAILED() - return self:Is( "FAILED" ) -end - ---- Is the @{Mission} **HOLD**. --- @param #MISSION self --- @return #boolean -function MISSION:IsHOLD() - return self:Is( "HOLD" ) -end - ---- Validates if the Mission has a Group --- @param #MISSION --- @return #boolean true if the Mission has a Group. -function MISSION:HasGroup( TaskGroup ) - local Has = false - - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - if Task:HasGroup( TaskGroup ) then - Has = true - break - end - end - - return Has -end - ---- @param #MISSION self --- @return #number -function MISSION:GetTasksRemaining() - -- Determine how many tasks are remaining. - local TasksRemaining = 0 - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - if Task:IsStateSuccess() or Task:IsStateFailed() then - else - TasksRemaining = TasksRemaining + 1 - end - end - return TasksRemaining -end - ---- @param #MISSION self --- @return #number -function MISSION:GetTaskTypes() - -- Determine how many tasks are remaining. - local TaskTypeList = {} - local TasksRemaining = 0 - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - local TaskType = Task:GetType() - TaskTypeList[TaskType] = TaskType - end - return TaskTypeList -end - - -function MISSION:AddPlayerName( PlayerName ) - self.PlayerNames = self.PlayerNames or {} - self.PlayerNames[PlayerName] = PlayerName - return self -end - -function MISSION:GetPlayerNames() - return self.PlayerNames -end - - ---- Create a briefing report of the Mission. --- @param #MISSION self --- @return #string -function MISSION:ReportBriefing() - - local Report = REPORT:New() - - -- List the name of the mission. - local Name = self:GetName() - - -- Determine the status of the mission. - local Status = "<" .. self:GetState() .. ">" - - Report:Add( string.format( '%s - %s - Mission Briefing Report', Name, Status ) ) - - Report:Add( self.MissionBriefing ) - - return Report:Text() -end - - ---- Create a status report of the Mission. --- This reports provides a one liner of the mission status. It indicates how many players and how many Tasks. --- --- Mission "" - Status "" --- - Task Types: , --- - Planned Tasks (xp) --- - Assigned Tasks(xp) --- - Success Tasks (xp) --- - Hold Tasks (xp) --- - Cancelled Tasks (xp) --- - Aborted Tasks (xp) --- - Failed Tasks (xp) --- --- @param #MISSION self --- @return #string -function MISSION:ReportStatus() - - local Report = REPORT:New() - - -- List the name of the mission. - local Name = self:GetName() - - -- Determine the status of the mission. - local Status = "<" .. self:GetState() .. ">" - - Report:Add( string.format( '%s - Status "%s"', Name, Status ) ) - - local TaskTypes = self:GetTaskTypes() - - Report:Add( string.format( " - Task Types: %s", table.concat(TaskTypes, ", " ) ) ) - - local TaskStatusList = { "Planned", "Assigned", "Success", "Hold", "Cancelled", "Aborted", "Failed" } - - for TaskStatusID, TaskStatus in pairs( TaskStatusList ) do - local TaskCount = 0 - local TaskPlayerCount = 0 - -- Determine how many tasks are remaining. - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - if Task:Is( TaskStatus ) then - TaskCount = TaskCount + 1 - TaskPlayerCount = TaskPlayerCount + Task:GetPlayerCount() - end - end - if TaskCount > 0 then - Report:Add( string.format( " - %02d %s Tasks (%dp)", TaskCount, TaskStatus, TaskPlayerCount ) ) - end - end - - return Report:Text() -end - - ---- Create an active player report of the Mission. --- This reports provides a one liner of the mission status. It indicates how many players and how many Tasks. --- --- Mission "" - - Active Players Report --- - Player ": Task , Task --- - Player : Task , Task --- - .. --- --- @param #MISSION self --- @return #string -function MISSION:ReportPlayersPerTask( ReportGroup ) - - local Report = REPORT:New() - - -- List the name of the mission. - local Name = self:GetName() - - -- Determine the status of the mission. - local Status = "<" .. self:GetState() .. ">" - - Report:Add( string.format( '%s - %s - Players per Task Report', Name, Status ) ) - - local PlayerList = {} - - -- Determine how many tasks are remaining. - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - local PlayerNames = Task:GetPlayerNames() - for PlayerName, PlayerGroup in pairs( PlayerNames ) do - PlayerList[PlayerName] = Task:GetName() - end - - end - - for PlayerName, TaskName in pairs( PlayerList ) do - Report:Add( string.format( ' - Player (%s): Task "%s"', PlayerName, TaskName ) ) - end - - return Report:Text() -end - ---- Create an Mission Progress report of the Mission. --- This reports provides a one liner per player of the mission achievements per task. --- --- Mission "" - - Active Players Report --- - Player : Task : --- - Player : Task : --- - .. --- --- @param #MISSION self --- @return #string -function MISSION:ReportPlayersProgress( ReportGroup ) - - local Report = REPORT:New() - - -- List the name of the mission. - local Name = self:GetName() - - -- Determine the status of the mission. - local Status = "<" .. self:GetState() .. ">" - - Report:Add( string.format( '%s - %s - Players per Task Progress Report', Name, Status ) ) - - local PlayerList = {} - - -- Determine how many tasks are remaining. - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - local TaskGoalTotal = Task:GetGoalTotal() or 0 - local TaskName = Task:GetName() - PlayerList[TaskName] = PlayerList[TaskName] or {} - if TaskGoalTotal ~= 0 then - local PlayerNames = self:GetPlayerNames() - for PlayerName, PlayerData in pairs( PlayerNames ) do - PlayerList[TaskName][PlayerName] = string.format( 'Player (%s): Task "%s": %d%%', PlayerName, TaskName, Task:GetPlayerProgress( PlayerName ) * 100 / TaskGoalTotal ) - end - else - PlayerList[TaskName]["_"] = string.format( 'Player (---): Task "%s": %d%%', TaskName, 0 ) - end - - end - - for TaskName, TaskData in pairs( PlayerList ) do - for PlayerName, TaskText in pairs( TaskData ) do - Report:Add( string.format( ' - %s', TaskText ) ) - end - end - - return Report:Text() -end - - ---- Create a summary report of the Mission (one line). --- @param #MISSION self --- @param Wrapper.Group#GROUP ReportGroup --- @return #string -function MISSION:ReportSummary( ReportGroup ) - - local Report = REPORT:New() - - -- List the name of the mission. - local Name = self:GetName() - - -- Determine the status of the mission. - local Status = "<" .. self:GetState() .. ">" - - Report:Add( string.format( '%s - %s - Task Overview Report', Name, Status ) ) - - -- Determine how many tasks are remaining. - for TaskID, Task in UTILS.spairs( self:GetTasks(), function( t, a, b ) return t[a]:ReportOrder( ReportGroup ) < t[b]:ReportOrder( ReportGroup ) end ) do - local Task = Task -- Tasking.Task#TASK - Report:Add( "- " .. Task:ReportSummary( ReportGroup ) ) - end - - return Report:Text() -end - ---- Create a overview report of the Mission (multiple lines). --- @param #MISSION self --- @return #string -function MISSION:ReportOverview( ReportGroup, TaskStatus ) - - self:F( { TaskStatus = TaskStatus } ) - - local Report = REPORT:New() - - -- List the name of the mission. - local Name = self:GetName() - - -- Determine the status of the mission. - local Status = "<" .. self:GetState() .. ">" - - Report:Add( string.format( '%s - %s - %s Tasks Report', Name, Status, TaskStatus ) ) - - -- Determine how many tasks are remaining. - local Tasks = 0 - for TaskID, Task in UTILS.spairs( self:GetTasks(), function( t, a, b ) return t[a]:ReportOrder( ReportGroup ) < t[b]:ReportOrder( ReportGroup ) end ) do - local Task = Task -- Tasking.Task#TASK - if Task:Is( TaskStatus ) then - Report:Add( string.rep( "-", 140 ) ) - Report:Add( " - " .. Task:ReportOverview( ReportGroup ) ) - end - Tasks = Tasks + 1 - if Tasks >= 8 then - break - end - end - - return Report:Text() -end - ---- Create a detailed report of the Mission, listing all the details of the Task. --- @param #MISSION self --- @return #string -function MISSION:ReportDetails( ReportGroup ) - - local Report = REPORT:New() - - -- List the name of the mission. - local Name = self:GetName() - - -- Determine the status of the mission. - local Status = "<" .. self:GetState() .. ">" - - Report:Add( string.format( '%s - %s - Task Detailed Report', Name, Status ) ) - - -- Determine how many tasks are remaining. - local TasksRemaining = 0 - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - Report:Add( Task:ReportDetails( ReportGroup ) ) - end - - return Report:Text() -end - ---- Get all the TASKs from the Mission. This function is useful in GoalFunctions. --- @return {TASK,...} Structure of TASKS with the @{TASK} number as the key. --- @usage --- -- Get Tasks from the Mission. --- Tasks = Mission:GetTasks() --- env.info( "Task 2 Completion = " .. Tasks[2]:GetGoalPercentage() .. "%" ) -function MISSION:GetTasks() - - return self.Tasks -end - ---- Reports the briefing. --- @param #MISSION self --- @param Wrapper.Group#GROUP ReportGroup The group to which the report needs to be sent. -function MISSION:MenuReportBriefing( ReportGroup ) - - local Report = self:ReportBriefing() - - self:GetCommandCenter():MessageTypeToGroup( Report, ReportGroup, MESSAGE.Type.Briefing ) -end - - - ---- Report the task summary. --- @param #MISSION self --- @param Wrapper.Group#GROUP ReportGroup -function MISSION:MenuReportTasksSummary( ReportGroup ) - - local Report = self:ReportSummary( ReportGroup ) - - self:GetCommandCenter():MessageTypeToGroup( Report, ReportGroup, MESSAGE.Type.Overview ) -end - - - - ---- @param #MISSION self --- @param #string TaskStatus The status --- @param Wrapper.Group#GROUP ReportGroup -function MISSION:MenuReportTasksPerStatus( ReportGroup, TaskStatus ) - - local Report = self:ReportOverview( ReportGroup, TaskStatus ) - - self:GetCommandCenter():MessageTypeToGroup( Report, ReportGroup, MESSAGE.Type.Overview ) -end - - ---- @param #MISSION self --- @param Wrapper.Group#GROUP ReportGroup -function MISSION:MenuReportPlayersPerTask( ReportGroup ) - - local Report = self:ReportPlayersPerTask() - - self:GetCommandCenter():MessageTypeToGroup( Report, ReportGroup, MESSAGE.Type.Overview ) -end - ---- @param #MISSION self --- @param Wrapper.Group#GROUP ReportGroup -function MISSION:MenuReportPlayersProgress( ReportGroup ) - - local Report = self:ReportPlayersProgress() - - self:GetCommandCenter():MessageTypeToGroup( Report, ReportGroup, MESSAGE.Type.Overview ) -end - - - - - ---- **Tasking** -- This module contains the TASK class, the main engine to run human taskings. --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- ==== --- --- @module Task - ---- @type TASK --- @field Core.Scheduler#SCHEDULER TaskScheduler --- @field Tasking.Mission#MISSION Mission --- @field Core.Set#SET_GROUP SetGroup The Set of Groups assigned to the Task --- @field Core.Fsm#FSM_PROCESS FsmTemplate --- @field Tasking.Mission#MISSION Mission --- @field Tasking.CommandCenter#COMMANDCENTER CommandCenter --- @extends Core.Fsm#FSM_TASK - ---- --- # TASK class, extends @{Base#BASE} --- --- ## The TASK class implements the methods for task orchestration within MOOSE. --- --- The class provides a couple of methods to: --- --- * @{#TASK.AssignToGroup}():Assign a task to a group (of players). --- * @{#TASK.AddProcess}():Add a @{Process} to a task. --- * @{#TASK.RemoveProcesses}():Remove a running @{Process} from a running task. --- * @{#TASK.SetStateMachine}():Set a @{Fsm} to a task. --- * @{#TASK.RemoveStateMachine}():Remove @{Fsm} from a task. --- * @{#TASK.HasStateMachine}():Enquire if the task has a @{Fsm} --- * @{#TASK.AssignToUnit}(): Assign a task to a unit. (Needs to be implemented in the derived classes from @{#TASK}. --- * @{#TASK.UnAssignFromUnit}(): Unassign the task from a unit. --- * @{#TASK.SetTimeOut}(): Set timer in seconds before task gets cancelled if not assigned. --- --- ## 1.2) Set and enquire task status (beyond the task state machine processing). --- --- A task needs to implement as a minimum the following task states: --- --- * **Success**: Expresses the successful execution and finalization of the task. --- * **Failed**: Expresses the failure of a task. --- * **Planned**: Expresses that the task is created, but not yet in execution and is not assigned yet. --- * **Assigned**: Expresses that the task is assigned to a Group of players, and that the task is in execution mode. --- --- A task may also implement the following task states: --- --- * **Rejected**: Expresses that the task is rejected by a player, who was requested to accept the task. --- * **Cancelled**: Expresses that the task is cancelled by HQ or through a logical situation where a cancellation of the task is required. --- --- A task can implement more statusses than the ones outlined above. Please consult the documentation of the specific tasks to understand the different status modelled. --- --- The status of tasks can be set by the methods **State** followed by the task status. An example is `StateAssigned()`. --- The status of tasks can be enquired by the methods **IsState** followed by the task status name. An example is `if IsStateAssigned() then`. --- --- ## 1.3) Add scoring when reaching a certain task status: --- --- Upon reaching a certain task status in a task, additional scoring can be given. If the Mission has a scoring system attached, the scores will be added to the mission scoring. --- Use the method @{#TASK.AddScore}() to add scores when a status is reached. --- --- ## 1.4) Task briefing: --- --- A task briefing can be given that is shown to the player when he is assigned to the task. --- --- @field #TASK TASK --- -TASK = { - ClassName = "TASK", - TaskScheduler = nil, - ProcessClasses = {}, -- The container of the Process classes that will be used to create and assign new processes for the task to ProcessUnits. - Processes = {}, -- The container of actual process objects instantiated and assigned to ProcessUnits. - Players = nil, - Scores = {}, - Menu = {}, - SetGroup = nil, - FsmTemplate = nil, - Mission = nil, - CommandCenter = nil, - TimeOut = 0, - AssignedGroups = {}, -} - ---- FSM PlayerAborted event handler prototype for TASK. --- @function [parent=#TASK] OnAfterPlayerAborted --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he went back to spectators or left the mission. --- @param #string PlayerName The name of the Player. - ---- FSM PlayerCrashed event handler prototype for TASK. --- @function [parent=#TASK] OnAfterPlayerCrashed --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he crashed in the mission. --- @param #string PlayerName The name of the Player. - ---- FSM PlayerDead event handler prototype for TASK. --- @function [parent=#TASK] OnAfterPlayerDead --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he died in the mission. --- @param #string PlayerName The name of the Player. - ---- FSM Fail synchronous event function for TASK. --- Use this event to Fail the Task. --- @function [parent=#TASK] Fail --- @param #TASK self - ---- FSM Fail asynchronous event function for TASK. --- Use this event to Fail the Task. --- @function [parent=#TASK] __Fail --- @param #TASK self - ---- FSM Abort synchronous event function for TASK. --- Use this event to Abort the Task. --- @function [parent=#TASK] Abort --- @param #TASK self - ---- FSM Abort asynchronous event function for TASK. --- Use this event to Abort the Task. --- @function [parent=#TASK] __Abort --- @param #TASK self - ---- FSM Success synchronous event function for TASK. --- Use this event to make the Task a Success. --- @function [parent=#TASK] Success --- @param #TASK self - ---- FSM Success asynchronous event function for TASK. --- Use this event to make the Task a Success. --- @function [parent=#TASK] __Success --- @param #TASK self - ---- FSM Cancel synchronous event function for TASK. --- Use this event to Cancel the Task. --- @function [parent=#TASK] Cancel --- @param #TASK self - ---- FSM Cancel asynchronous event function for TASK. --- Use this event to Cancel the Task. --- @function [parent=#TASK] __Cancel --- @param #TASK self - ---- FSM Replan synchronous event function for TASK. --- Use this event to Replan the Task. --- @function [parent=#TASK] Replan --- @param #TASK self - ---- FSM Replan asynchronous event function for TASK. --- Use this event to Replan the Task. --- @function [parent=#TASK] __Replan --- @param #TASK self - - ---- Instantiates a new TASK. Should never be used. Interface Class. --- @param #TASK self --- @param Tasking.Mission#MISSION Mission The mission wherein the Task is registered. --- @param Core.Set#SET_GROUP SetGroupAssign The set of groups for which the Task can be assigned. --- @param #string TaskName The name of the Task --- @param #string TaskType The type of the Task --- @return #TASK self -function TASK:New( Mission, SetGroupAssign, TaskName, TaskType, TaskBriefing ) - - local self = BASE:Inherit( self, FSM_TASK:New() ) -- Tasking.Task#TASK - - self:SetStartState( "Planned" ) - self:AddTransition( "Planned", "Assign", "Assigned" ) - self:AddTransition( "Assigned", "AssignUnit", "Assigned" ) - self:AddTransition( "Assigned", "Success", "Success" ) - self:AddTransition( "Assigned", "Hold", "Hold" ) - self:AddTransition( "Assigned", "Fail", "Failed" ) - self:AddTransition( "Assigned", "Abort", "Aborted" ) - self:AddTransition( "Assigned", "Cancel", "Cancelled" ) - self:AddTransition( "Assigned", "Goal", "*" ) - - --- Goal Handler OnBefore for TASK - -- @function [parent=#TASK] OnBeforeGoal - -- @param #TASK self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- Goal Handler OnAfter for TASK - -- @function [parent=#TASK] OnAfterGoal - -- @param #TASK self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable - -- @param #string From - -- @param #string Event - -- @param #string To - - --- Goal Trigger for TASK - -- @function [parent=#TASK] Goal - -- @param #TASK self - - --- Goal Asynchronous Trigger for TASK - -- @function [parent=#TASK] __Goal - -- @param #TASK self - -- @param #number Delay - - - - self:AddTransition( "*", "PlayerCrashed", "*" ) - self:AddTransition( "*", "PlayerAborted", "*" ) - self:AddTransition( "*", "PlayerDead", "*" ) - self:AddTransition( { "Failed", "Aborted", "Cancelled" }, "Replan", "Planned" ) - self:AddTransition( "*", "TimeOut", "Cancelled" ) - - self:E( "New TASK " .. TaskName ) - - self.Processes = {} - self.Fsm = {} - - self.Mission = Mission - self.CommandCenter = Mission:GetCommandCenter() - - self.SetGroup = SetGroupAssign - - self:SetType( TaskType ) - self:SetName( TaskName ) - self:SetID( Mission:GetNextTaskID( self ) ) -- The Mission orchestrates the task sequences .. - - self:SetBriefing( TaskBriefing ) - - self.FsmTemplate = self.FsmTemplate or FSM_PROCESS:New() - - self.TaskInfo = {} - - self.TaskProgress = {} - - return self -end - ---- Get the Task FSM Process Template --- @param #TASK self --- @return Core.Fsm#FSM_PROCESS -function TASK:GetUnitProcess( TaskUnit ) - - if TaskUnit then - return self:GetStateMachine( TaskUnit ) - else - return self.FsmTemplate - end -end - ---- Sets the Task FSM Process Template --- @param #TASK self --- @param Core.Fsm#FSM_PROCESS -function TASK:SetUnitProcess( FsmTemplate ) - - self.FsmTemplate = FsmTemplate -end - ---- Add a PlayerUnit to join the Task. --- For each Group within the Task, the Unit is checked if it can join the Task. --- If the Unit was not part of the Task, false is returned. --- If the Unit is part of the Task, true is returned. --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission. --- @param Wrapper.Group#GROUP PlayerGroup The GROUP of the player joining the Mission. --- @return #boolean true if Unit is part of the Task. -function TASK:JoinUnit( PlayerUnit, PlayerGroup ) - self:F( { PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } ) - - local PlayerUnitAdded = false - - local PlayerGroups = self:GetGroups() - - -- Is the PlayerGroup part of the PlayerGroups? - if PlayerGroups:IsIncludeObject( PlayerGroup ) then - - -- Check if the PlayerGroup is already assigned to the Task. If yes, the PlayerGroup is added to the Task. - -- If the PlayerGroup is not assigned to the Task, the menu needs to be set. In that case, the PlayerUnit will become the GroupPlayer leader. - if self:IsStatePlanned() or self:IsStateReplanned() then - --self:SetMenuForGroup( PlayerGroup ) - --self:MessageToGroups( PlayerUnit:GetPlayerName() .. " is planning to join Task " .. self:GetName() ) - end - if self:IsStateAssigned() then - local IsGroupAssigned = self:IsGroupAssigned( PlayerGroup ) - self:E( { IsGroupAssigned = IsGroupAssigned } ) - if IsGroupAssigned then - self:AssignToUnit( PlayerUnit ) - self:MessageToGroups( PlayerUnit:GetPlayerName() .. " joined Task " .. self:GetName() ) - end - end - end - - return PlayerUnitAdded -end - ---- Abort a PlayerUnit from a Task. --- If the Unit was not part of the Task, false is returned. --- If the Unit is part of the Task, true is returned. --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player aborting the Task. --- @return #TASK -function TASK:AbortGroup( PlayerGroup ) - self:F( { PlayerGroup = PlayerGroup } ) - - local PlayerGroups = self:GetGroups() - - -- Is the PlayerGroup part of the PlayerGroups? - if PlayerGroups:IsIncludeObject( PlayerGroup ) then - - -- Check if the PlayerGroup is already assigned to the Task. If yes, the PlayerGroup is aborted from the Task. - -- If the PlayerUnit was the last unit of the PlayerGroup, the menu needs to be removed from the Group. - if self:IsStateAssigned() then - local IsGroupAssigned = self:IsGroupAssigned( PlayerGroup ) - self:E( { IsGroupAssigned = IsGroupAssigned } ) - if IsGroupAssigned then - local PlayerName = PlayerGroup:GetUnit(1):GetPlayerName() - --self:MessageToGroups( PlayerName .. " aborted Task " .. self:GetName() ) - self:UnAssignFromGroup( PlayerGroup ) - --self:Abort() - - -- Now check if the task needs to go to hold... - -- It will go to hold, if there are no players in the mission... - - PlayerGroups:Flush() - local IsRemaining = false - for GroupName, AssignedGroup in pairs( PlayerGroups:GetSet() or {} ) do - if self:IsGroupAssigned( AssignedGroup ) == true then - IsRemaining = true - self:F( { Task = self:GetName(), IsRemaining = IsRemaining } ) - break - end - end - - self:F( { Task = self:GetName(), IsRemaining = IsRemaining } ) - if IsRemaining == false then - self:Abort() - end - - self:PlayerAborted( PlayerGroup:GetUnit(1) ) - end - - end - end - - return self -end - ---- A PlayerUnit crashed in a Task. Abort the Player. --- If the Unit was not part of the Task, false is returned. --- If the Unit is part of the Task, true is returned. --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player aborting the Task. --- @return #TASK -function TASK:CrashGroup( PlayerGroup ) - self:F( { PlayerGroup = PlayerGroup } ) - - local PlayerGroups = self:GetGroups() - - -- Is the PlayerGroup part of the PlayerGroups? - if PlayerGroups:IsIncludeObject( PlayerGroup ) then - - -- Check if the PlayerGroup is already assigned to the Task. If yes, the PlayerGroup is aborted from the Task. - -- If the PlayerUnit was the last unit of the PlayerGroup, the menu needs to be removed from the Group. - if self:IsStateAssigned() then - local IsGroupAssigned = self:IsGroupAssigned( PlayerGroup ) - self:E( { IsGroupAssigned = IsGroupAssigned } ) - if IsGroupAssigned then - local PlayerName = PlayerGroup:GetUnit(1):GetPlayerName() - self:MessageToGroups( PlayerName .. " crashed! " ) - self:UnAssignFromGroup( PlayerGroup ) - - -- Now check if the task needs to go to hold... - -- It will go to hold, if there are no players in the mission... - - PlayerGroups:Flush() - local IsRemaining = false - for GroupName, AssignedGroup in pairs( PlayerGroups:GetSet() or {} ) do - if self:IsGroupAssigned( AssignedGroup ) == true then - IsRemaining = true - self:F( { Task = self:GetName(), IsRemaining = IsRemaining } ) - break - end - end - - self:F( { Task = self:GetName(), IsRemaining = IsRemaining } ) - if IsRemaining == false then - self:Abort() - end - - self:PlayerCrashed( PlayerGroup:GetUnit(1) ) - end - - end - end - - return self -end - - - ---- Gets the Mission to where the TASK belongs. --- @param #TASK self --- @return Tasking.Mission#MISSION -function TASK:GetMission() - - return self.Mission -end - - ---- Gets the SET_GROUP assigned to the TASK. --- @param #TASK self --- @return Core.Set#SET_GROUP -function TASK:GetGroups() - return self.SetGroup -end - -do -- Group Assignment - - --- Returns if the @{Task} is assigned to the Group. - -- @param #TASK self - -- @param Wrapper.Group#GROUP TaskGroup - -- @return #boolean - function TASK:IsGroupAssigned( TaskGroup ) - - local TaskGroupName = TaskGroup:GetName() - - if self.AssignedGroups[TaskGroupName] then - self:T( { "Task is assigned to:", TaskGroup:GetName() } ) - return true - end - - self:T( { "Task is not assigned to:", TaskGroup:GetName() } ) - return false - end - - - --- Set @{Group} assigned to the @{Task}. - -- @param #TASK self - -- @param Wrapper.Group#GROUP TaskGroup - -- @return #TASK - function TASK:SetGroupAssigned( TaskGroup ) - - local TaskName = self:GetName() - local TaskGroupName = TaskGroup:GetName() - - self.AssignedGroups[TaskGroupName] = TaskGroup - self:E( string.format( "Task %s is assigned to %s", TaskName, TaskGroupName ) ) - - -- Set the group to be assigned at mission level. This allows to decide the menu options on mission level for this group. - self:GetMission():SetGroupAssigned( TaskGroup ) - - local SetAssignedGroups = self:GetGroups() - --- SetAssignedGroups:ForEachGroup( --- function( AssignedGroup ) --- if self:IsGroupAssigned(AssignedGroup) then --- self:GetMission():GetCommandCenter():MessageToGroup( string.format( "Task %s is assigned to group %s.", TaskName, TaskGroupName ), AssignedGroup ) --- else --- self:GetMission():GetCommandCenter():MessageToGroup( string.format( "Task %s is assigned to your group.", TaskName ), AssignedGroup ) --- end --- end --- ) - - return self - end - - --- Clear the @{Group} assignment from the @{Task}. - -- @param #TASK self - -- @param Wrapper.Group#GROUP TaskGroup - -- @return #TASK - function TASK:ClearGroupAssignment( TaskGroup ) - - local TaskName = self:GetName() - local TaskGroupName = TaskGroup:GetName() - - self.AssignedGroups[TaskGroupName] = nil - --self:E( string.format( "Task %s is unassigned to %s", TaskName, TaskGroupName ) ) - - -- Set the group to be assigned at mission level. This allows to decide the menu options on mission level for this group. - self:GetMission():ClearGroupAssignment( TaskGroup ) - - local SetAssignedGroups = self:GetGroups() - - SetAssignedGroups:ForEachGroup( - function( AssignedGroup ) - if self:IsGroupAssigned(AssignedGroup) then - --self:GetMission():GetCommandCenter():MessageToGroup( string.format( "Task %s is unassigned from group %s.", TaskName, TaskGroupName ), AssignedGroup ) - else - --self:GetMission():GetCommandCenter():MessageToGroup( string.format( "Task %s is unassigned from your group.", TaskName ), AssignedGroup ) - end - end - ) - - return self - end - -end - -do -- Group Assignment - - --- Assign the @{Task} to a @{Group}. - -- @param #TASK self - -- @param Wrapper.Group#GROUP TaskGroup - -- @return #TASK - function TASK:AssignToGroup( TaskGroup ) - self:F( TaskGroup:GetName() ) - - local TaskGroupName = TaskGroup:GetName() - local Mission = self:GetMission() - local CommandCenter = Mission:GetCommandCenter() - - self:SetGroupAssigned( TaskGroup ) - - local TaskUnits = TaskGroup:GetUnits() - for UnitID, UnitData in pairs( TaskUnits ) do - local TaskUnit = UnitData -- Wrapper.Unit#UNIT - local PlayerName = TaskUnit:GetPlayerName() - self:E(PlayerName) - if PlayerName ~= nil and PlayerName ~= "" then - self:AssignToUnit( TaskUnit ) - CommandCenter:MessageToGroup( - string.format( 'Task "%s": Briefing for player (%s):\n%s', - self:GetName(), - PlayerName, - self:GetBriefing() - ), TaskGroup - ) - end - end - - CommandCenter:SetMenu() - - return self - end - - --- UnAssign the @{Task} from a @{Group}. - -- @param #TASK self - -- @param Wrapper.Group#GROUP TaskGroup - function TASK:UnAssignFromGroup( TaskGroup ) - self:F2( { TaskGroup = TaskGroup:GetName() } ) - - self:ClearGroupAssignment( TaskGroup ) - - local TaskUnits = TaskGroup:GetUnits() - for UnitID, UnitData in pairs( TaskUnits ) do - local TaskUnit = UnitData -- Wrapper.Unit#UNIT - local PlayerName = TaskUnit:GetPlayerName() - if PlayerName ~= nil and PlayerName ~= "" then -- Only remove units that have players! - self:UnAssignFromUnit( TaskUnit ) - end - end - - local Mission = self:GetMission() - local CommandCenter = Mission:GetCommandCenter() - CommandCenter:SetMenu() - end -end - - ---- --- @param #TASK self --- @param Wrapper.Group#GROUP FindGroup --- @return #boolean -function TASK:HasGroup( FindGroup ) - - local SetAttackGroup = self:GetGroups() - return SetAttackGroup:FindGroup(FindGroup) - -end - ---- Assign the @{Task} to an alive @{Unit}. --- @param #TASK self --- @param Wrapper.Unit#UNIT TaskUnit --- @return #TASK self -function TASK:AssignToUnit( TaskUnit ) - self:F( TaskUnit:GetName() ) - - local FsmTemplate = self:GetUnitProcess() - - -- Assign a new FsmUnit to TaskUnit. - local FsmUnit = self:SetStateMachine( TaskUnit, FsmTemplate:Copy( TaskUnit, self ) ) -- Core.Fsm#FSM_PROCESS - - FsmUnit:SetStartState( "Planned" ) - - FsmUnit:Accept() -- Each Task needs to start with an Accept event to start the flow. - - return self -end - ---- UnAssign the @{Task} from an alive @{Unit}. --- @param #TASK self --- @param Wrapper.Unit#UNIT TaskUnit --- @return #TASK self -function TASK:UnAssignFromUnit( TaskUnit ) - self:F( TaskUnit:GetName() ) - - self:RemoveStateMachine( TaskUnit ) - - return self -end - ---- Sets the TimeOut for the @{Task}. If @{Task} stayed planned for longer than TimeOut, it gets into Cancelled status. --- @param #TASK self --- @param #integer Timer in seconds --- @return #TASK self -function TASK:SetTimeOut ( Timer ) - self:F( Timer ) - self.TimeOut = Timer - self:__TimeOut( self.TimeOut ) - return self -end - ---- Send a message of the @{Task} to the assigned @{Group}s. --- @param #TASK self -function TASK:MessageToGroups( Message ) - self:F( { Message = Message } ) - - local Mission = self:GetMission() - local CC = Mission:GetCommandCenter() - - for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do - local TaskGroup = TaskGroup -- Wrapper.Group#GROUP - CC:MessageToGroup( Message, TaskGroup, TaskGroup:GetName() ) - end -end - - ---- Send the briefng message of the @{Task} to the assigned @{Group}s. --- @param #TASK self -function TASK:SendBriefingToAssignedGroups() - self:F2() - - for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do - - if self:IsGroupAssigned( TaskGroup ) then - TaskGroup:Message( self.TaskBriefing, 60 ) - end - end -end - - ---- UnAssign the @{Task} from the @{Group}s. --- @param #TASK self -function TASK:UnAssignFromGroups() - self:F2() - - for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do - if self:IsGroupAssigned(TaskGroup) then - self:UnAssignFromGroup( TaskGroup ) - end - end -end - - - ---- Returns if the @{Task} has still alive and assigned Units. --- @param #TASK self --- @return #boolean -function TASK:HasAliveUnits() - self:F() - - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - if self:IsStateAssigned() then - if self:IsGroupAssigned( TaskGroup ) then - for TaskUnitID, TaskUnit in pairs( TaskGroup:GetUnits() ) do - if TaskUnit:IsAlive() then - self:T( { HasAliveUnits = true } ) - return true - end - end - end - end - end - - self:T( { HasAliveUnits = false } ) - return false -end - ---- Set the menu options of the @{Task} to all the groups in the SetGroup. --- @param #TASK self --- @param #number MenuTime --- @return #TASK -function TASK:SetMenu( MenuTime ) --R2.1 Mission Reports and Task Reports added. Fixes issue #424. - self:F( { self:GetName(), MenuTime } ) - - --self.SetGroup:Flush() - for TaskGroupID, TaskGroupData in pairs( self.SetGroup:GetSet() ) do - local TaskGroup = TaskGroupData -- Wrapper.Group#GROUP - if TaskGroup:IsAlive() and TaskGroup:GetPlayerNames() then - - -- Set Mission Menus - - local Mission = self:GetMission() - local MissionMenu = Mission:GetMenu( TaskGroup ) - if MissionMenu then - self:SetMenuForGroup( TaskGroup, MenuTime ) - end - end - end -end - - - ---- Set the Menu for a Group --- @param #TASK self --- @param #number MenuTime --- @return #TASK -function TASK:SetMenuForGroup( TaskGroup, MenuTime ) - - if self:IsStatePlanned() or self:IsStateAssigned() then - self:SetPlannedMenuForGroup( TaskGroup, MenuTime ) - if self:IsGroupAssigned( TaskGroup ) then - self:SetAssignedMenuForGroup( TaskGroup, MenuTime ) - end - end -end - - ---- Set the planned menu option of the @{Task}. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @param #string MenuText The menu text. --- @param #number MenuTime --- @return #TASK self -function TASK:SetPlannedMenuForGroup( TaskGroup, MenuTime ) - self:F( TaskGroup:GetName() ) - - local Mission = self:GetMission() - local MissionName = Mission:GetName() - local CommandCenter = Mission:GetCommandCenter() - local CommandCenterMenu = CommandCenter:GetMenu() - - local TaskType = self:GetType() --- local TaskThreatLevel = self.TaskInfo["ThreatLevel"] --- local TaskThreatLevelString = TaskThreatLevel and " [" .. string.rep( "■", TaskThreatLevel ) .. "]" or " []" - local TaskPlayerCount = self:GetPlayerCount() - local TaskPlayerString = string.format( " (%dp)", TaskPlayerCount ) - local TaskText = string.format( "%s%s", self:GetName(), TaskPlayerString ) --, TaskThreatLevelString ) - local TaskName = string.format( "%s", self:GetName() ) - - local MissionMenu = Mission:GetMenu( TaskGroup ) - --local MissionMenu = MENU_GROUP:New( TaskGroup, MissionName, CommandCenterMenu ):SetTime( MenuTime ) - - --local MissionMenu = Mission:GetMenu( TaskGroup ) - - self.MenuPlanned = self.MenuPlanned or {} - self.MenuPlanned[TaskGroup] = MENU_GROUP:New( TaskGroup, "Join Planned Task", MissionMenu, Mission.MenuReportTasksPerStatus, Mission, TaskGroup, "Planned" ):SetTime( MenuTime ):SetTag( "Tasking" ) - local TaskTypeMenu = MENU_GROUP:New( TaskGroup, TaskType, self.MenuPlanned[TaskGroup] ):SetTime( MenuTime ):SetTag( "Tasking" ):SetRemoveParent( true ) - local TaskTypeMenu = MENU_GROUP:New( TaskGroup, TaskText, TaskTypeMenu ):SetTime( MenuTime ):SetTag( "Tasking" ):SetRemoveParent( true ) - local ReportTaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Report Task Status" ), TaskTypeMenu, self.MenuTaskStatus, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" ):SetRemoveParent( true ) - - if not Mission:IsGroupAssigned( TaskGroup ) then - self:F( { "Replacing Join Task menu" } ) - local JoinTaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Join Task" ), TaskTypeMenu, self.MenuAssignToGroup, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" ):SetRemoveParent( true ) - local MarkTaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Mark Task on Map" ), TaskTypeMenu, self.MenuMarkToGroup, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" ):SetRemoveParent( true ) - end - - return self -end - ---- Set the assigned menu options of the @{Task}. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @param #number MenuTime --- @return #TASK self -function TASK:SetAssignedMenuForGroup( TaskGroup, MenuTime ) - self:F( { TaskGroup:GetName(), MenuTime } ) - - local Mission = self:GetMission() - local MissionName = Mission:GetName() - local CommandCenter = Mission:GetCommandCenter() - local CommandCenterMenu = CommandCenter:GetMenu() - - local TaskType = self:GetType() --- local TaskThreatLevel = self.TaskInfo["ThreatLevel"] --- local TaskThreatLevelString = TaskThreatLevel and " [" .. string.rep( "■", TaskThreatLevel ) .. "]" or " []" - local TaskPlayerCount = self:GetPlayerCount() - local TaskPlayerString = string.format( " (%dp)", TaskPlayerCount ) - local TaskText = string.format( "%s%s", self:GetName(), TaskPlayerString ) --, TaskThreatLevelString ) - local TaskName = string.format( "%s", self:GetName() ) - - local MissionMenu = Mission:GetMenu( TaskGroup ) --- local MissionMenu = MENU_GROUP:New( TaskGroup, MissionName, CommandCenterMenu ):SetTime( MenuTime ) --- local MissionMenu = Mission:GetMenu( TaskGroup ) - - self.MenuAssigned = self.MenuAssigned or {} - self.MenuAssigned[TaskGroup] = MENU_GROUP:New( TaskGroup, string.format( "Assigned Task %s", TaskName ), MissionMenu ):SetTime( MenuTime ):SetTag( "Tasking" ) - local TaskTypeMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Report Task Status" ), self.MenuAssigned[TaskGroup], self.MenuTaskStatus, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" ):SetRemoveParent( true ) - local TaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Abort Group from Task" ), self.MenuAssigned[TaskGroup], self.MenuTaskAbort, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" ):SetRemoveParent( true ) - - return self -end - ---- Remove the menu options of the @{Task} to all the groups in the SetGroup. --- @param #TASK self --- @param #number MenuTime --- @return #TASK -function TASK:RemoveMenu( MenuTime ) - self:F( { self:GetName(), MenuTime } ) - - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - local TaskGroup = TaskGroup -- Wrapper.Group#GROUP - self:RefreshMenus( TaskGroup, MenuTime ) - end -end - - ---- Remove the menu option of the @{Task} for a @{Group}. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @param #number MenuTime --- @return #TASK self -function TASK:RefreshMenus( TaskGroup, MenuTime ) - self:F( { TaskGroup:GetName(), MenuTime } ) - - local Mission = self:GetMission() - local MissionName = Mission:GetName() - local CommandCenter = Mission:GetCommandCenter() - local CommandCenterMenu = CommandCenter:GetMenu() - - local MissionMenu = Mission:GetMenu( TaskGroup ) - - local TaskName = self:GetName() - self.MenuPlanned = self.MenuPlanned or {} - local PlannedMenu = self.MenuPlanned[TaskGroup] - - self.MenuAssigned = self.MenuAssigned or {} - local AssignedMenu = self.MenuAssigned[TaskGroup] - - if PlannedMenu then - PlannedMenu:Remove( MenuTime , "Tasking") - end - - if AssignedMenu then - AssignedMenu:Remove( MenuTime, "Tasking" ) - end - -end - ---- Remove the assigned menu option of the @{Task} for a @{Group}. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @param #number MenuTime --- @return #TASK self -function TASK:RemoveAssignedMenuForGroup( TaskGroup ) - self:F() - - local Mission = self:GetMission() - local MissionName = Mission:GetName() - - local MissionMenu = Mission:GetMenu( TaskGroup ) - - if MissionMenu then - MissionMenu:RemoveSubMenus() - end - -end - ---- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup -function TASK:MenuAssignToGroup( TaskGroup ) - - self:E( "Join Task menu selected") - - self:AssignToGroup( TaskGroup ) -end - ---- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup -function TASK:MenuMarkToGroup( TaskGroup ) - - self:E( "Mark Task menu selected") - - self:UpdateTaskInfo() - - local Report = REPORT:New():SetIndent( 0 ) - - -- List the name of the Task. - local Name = self:GetName() - Report:Add( Name .. ": " .. self:GetTaskBriefing() ) - - for TaskInfoID, TaskInfo in pairs( self.TaskInfo, function( t, a, b ) return t[a].TaskInfoOrder < t[b].TaskInfoOrder end ) do - - local TaskInfoIDText = "" --string.format( "%s: ", TaskInfoID ) - - if type( TaskInfo.TaskInfoText ) == "string" then - if TaskInfoID == "Targets" then - else - Report:Add( TaskInfoIDText .. TaskInfo.TaskInfoText ) - end - elseif type( TaskInfo ) == "table" then - if TaskInfoID == "Coordinates" then - --local ToCoordinate = TaskInfo.TaskInfoText -- Core.Point#COORDINATE - --Report:Add( TaskInfoIDText .. ToCoordinate:ToString() ) - else - end - end - - end - - local Coordinate = self:GetInfo( "Coordinates" ) -- Core.Point#COORDINATE - - local Velocity = self.TargetSetUnit:GetVelocity() - local Heading = self.TargetSetUnit:GetHeading() - - Coordinate:SetHeading( Heading ) - Coordinate:SetVelocity( Velocity ) - - Report:Add( "Targets are" .. Coordinate:GetMovingText() .. "." ) - - local MarkText = Report:Text( ", " ) - - self:F( { Coordinate = Coordinate, MarkText = MarkText } ) - - Coordinate:MarkToGroup( MarkText, TaskGroup ) - --Coordinate:MarkToAll( Briefing ) -end - ---- Report the task status. --- @param #TASK self -function TASK:MenuTaskStatus( TaskGroup ) - - local ReportText = self:ReportDetails( TaskGroup ) - - self:T( ReportText ) - self:GetMission():GetCommandCenter():MessageTypeToGroup( ReportText, TaskGroup, MESSAGE.Type.Detailed ) - -end - ---- Report the task status. --- @param #TASK self -function TASK:MenuTaskAbort( TaskGroup ) - - self:AbortGroup( TaskGroup ) -end - - - ---- Returns the @{Task} name. --- @param #TASK self --- @return #string TaskName -function TASK:GetTaskName() - return self.TaskName -end - ---- Returns the @{Task} briefing. --- @param #TASK self --- @return #string Task briefing. -function TASK:GetTaskBriefing() - return self.TaskBriefing -end - - - - ---- Get the default or currently assigned @{Process} template with key ProcessName. --- @param #TASK self --- @param #string ProcessName --- @return Core.Fsm#FSM_PROCESS -function TASK:GetProcessTemplate( ProcessName ) - - local ProcessTemplate = self.ProcessClasses[ProcessName] - - return ProcessTemplate -end - - - --- TODO: Obscolete? ---- Fail processes from @{Task} with key @{Unit} --- @param #TASK self --- @param #string TaskUnitName --- @return #TASK self -function TASK:FailProcesses( TaskUnitName ) - - for ProcessID, ProcessData in pairs( self.Processes[TaskUnitName] ) do - local Process = ProcessData - Process.Fsm:Fail() - end -end - ---- Add a FiniteStateMachine to @{Task} with key Task@{Unit} --- @param #TASK self --- @param Wrapper.Unit#UNIT TaskUnit --- @param Core.Fsm#FSM_PROCESS Fsm --- @return #TASK self -function TASK:SetStateMachine( TaskUnit, Fsm ) - self:F2( { TaskUnit, self.Fsm[TaskUnit] ~= nil, Fsm:GetClassNameAndID() } ) - - self.Fsm[TaskUnit] = Fsm - - return Fsm -end - ---- Gets the FiniteStateMachine of @{Task} with key Task@{Unit} --- @param #TASK self --- @param Wrapper.Unit#UNIT TaskUnit --- @return Core.Fsm#FSM_PROCESS -function TASK:GetStateMachine( TaskUnit ) - self:F2( { TaskUnit, self.Fsm[TaskUnit] ~= nil } ) - - return self.Fsm[TaskUnit] -end - ---- Remove FiniteStateMachines from @{Task} with key Task@{Unit} --- @param #TASK self --- @param Wrapper.Unit#UNIT TaskUnit --- @return #TASK self -function TASK:RemoveStateMachine( TaskUnit ) - self:F( { TaskUnit = TaskUnit:GetName(), HasFsm = ( self.Fsm[TaskUnit] ~= nil ) } ) - - --self:E( self.Fsm ) - --for TaskUnitT, Fsm in pairs( self.Fsm ) do - --local Fsm = Fsm -- Core.Fsm#FSM_PROCESS - --self:E( TaskUnitT ) - --self.Fsm[TaskUnit] = nil - --end - - if self.Fsm[TaskUnit] then - self.Fsm[TaskUnit]:Remove() - self.Fsm[TaskUnit] = nil - end - - collectgarbage() - self:E( "Garbage Collected, Processes should be finalized now ...") -end - - ---- Checks if there is a FiniteStateMachine assigned to Task@{Unit} for @{Task} --- @param #TASK self --- @param Wrapper.Unit#UNIT TaskUnit --- @return #TASK self -function TASK:HasStateMachine( TaskUnit ) - self:F( { TaskUnit, self.Fsm[TaskUnit] ~= nil } ) - - return ( self.Fsm[TaskUnit] ~= nil ) -end - - ---- Gets the Scoring of the task --- @param #TASK self --- @return Functional.Scoring#SCORING Scoring -function TASK:GetScoring() - return self.Mission:GetScoring() -end - - ---- Gets the Task Index, which is a combination of the Task type, the Task name. --- @param #TASK self --- @return #string The Task ID -function TASK:GetTaskIndex() - - local TaskType = self:GetType() - local TaskName = self:GetName() - - return TaskType .. "." .. TaskName -end - ---- Sets the Name of the Task --- @param #TASK self --- @param #string TaskName -function TASK:SetName( TaskName ) - self.TaskName = TaskName -end - ---- Gets the Name of the Task --- @param #TASK self --- @return #string The Task Name -function TASK:GetName() - return self.TaskName -end - ---- Sets the Type of the Task --- @param #TASK self --- @param #string TaskType -function TASK:SetType( TaskType ) - self.TaskType = TaskType -end - ---- Sets the Information on the Task --- @param #TASK self --- @param #string TaskInfo The key and title of the task information. --- @param #string TaskInfoText The Task info text. --- @param #number TaskInfoOrder The ordering, a number between 0 and 99. -function TASK:SetInfo( TaskInfo, TaskInfoText, TaskInfoOrder ) - - self.TaskInfo = self.TaskInfo or {} - self.TaskInfo[TaskInfo] = self.TaskInfo[TaskInfo] or {} - self.TaskInfo[TaskInfo].TaskInfoText = TaskInfoText - self.TaskInfo[TaskInfo].TaskInfoOrder = TaskInfoOrder -end - ---- Gets the Information of the Task --- @param #TASK self --- @param #string TaskInfo The key and title of the task information. --- @return #string TaskInfoText The Task info text. -function TASK:GetInfo( TaskInfo ) - - self.TaskInfo = self.TaskInfo or {} - self.TaskInfo[TaskInfo] = self.TaskInfo[TaskInfo] or {} - return self.TaskInfo[TaskInfo].TaskInfoText -end - ---- Gets the Type of the Task --- @param #TASK self --- @return #string TaskType -function TASK:GetType() - return self.TaskType -end - ---- Sets the ID of the Task --- @param #TASK self --- @param #string TaskID -function TASK:SetID( TaskID ) - self.TaskID = TaskID -end - ---- Gets the ID of the Task --- @param #TASK self --- @return #string TaskID -function TASK:GetID() - return self.TaskID -end - - ---- Sets a @{Task} to status **Success**. --- @param #TASK self -function TASK:StateSuccess() - self:SetState( self, "State", "Success" ) - return self -end - ---- Is the @{Task} status **Success**. --- @param #TASK self -function TASK:IsStateSuccess() - return self:Is( "Success" ) -end - ---- Sets a @{Task} to status **Failed**. --- @param #TASK self -function TASK:StateFailed() - self:SetState( self, "State", "Failed" ) - return self -end - ---- Is the @{Task} status **Failed**. --- @param #TASK self -function TASK:IsStateFailed() - return self:Is( "Failed" ) -end - ---- Sets a @{Task} to status **Planned**. --- @param #TASK self -function TASK:StatePlanned() - self:SetState( self, "State", "Planned" ) - return self -end - ---- Is the @{Task} status **Planned**. --- @param #TASK self -function TASK:IsStatePlanned() - return self:Is( "Planned" ) -end - ---- Sets a @{Task} to status **Aborted**. --- @param #TASK self -function TASK:StateAborted() - self:SetState( self, "State", "Aborted" ) - return self -end - ---- Is the @{Task} status **Aborted**. --- @param #TASK self -function TASK:IsStateAborted() - return self:Is( "Aborted" ) -end - ---- Sets a @{Task} to status **Cancelled**. --- @param #TASK self -function TASK:StateCancelled() - self:SetState( self, "State", "Cancelled" ) - return self -end - ---- Is the @{Task} status **Cancelled**. --- @param #TASK self -function TASK:IsStateCancelled() - return self:Is( "Cancelled" ) -end - ---- Sets a @{Task} to status **Assigned**. --- @param #TASK self -function TASK:StateAssigned() - self:SetState( self, "State", "Assigned" ) - return self -end - ---- Is the @{Task} status **Assigned**. --- @param #TASK self -function TASK:IsStateAssigned() - return self:Is( "Assigned" ) -end - ---- Sets a @{Task} to status **Hold**. --- @param #TASK self -function TASK:StateHold() - self:SetState( self, "State", "Hold" ) - return self -end - ---- Is the @{Task} status **Hold**. --- @param #TASK self -function TASK:IsStateHold() - return self:Is( "Hold" ) -end - ---- Sets a @{Task} to status **Replanned**. --- @param #TASK self -function TASK:StateReplanned() - self:SetState( self, "State", "Replanned" ) - return self -end - ---- Is the @{Task} status **Replanned**. --- @param #TASK self -function TASK:IsStateReplanned() - return self:Is( "Replanned" ) -end - ---- Gets the @{Task} status. --- @param #TASK self -function TASK:GetStateString() - return self:GetState( self, "State" ) -end - ---- Sets a @{Task} briefing. --- @param #TASK self --- @param #string TaskBriefing --- @return #TASK self -function TASK:SetBriefing( TaskBriefing ) - self:E(TaskBriefing) - self.TaskBriefing = TaskBriefing - return self -end - ---- Gets the @{Task} briefing. --- @param #TASK self --- @return #string The briefing text. -function TASK:GetBriefing() - return self.TaskBriefing -end - - - - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onenterAssigned( From, Event, To, PlayerUnit, PlayerName ) - - - --- This test is required, because the state transition will be fired also when the state does not change in case of an event. - if From ~= "Assigned" then - self:E( { From, Event, To, PlayerUnit:GetName(), PlayerName } ) - - self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " is assigned." ) - - -- Set the total Progress to be achieved. - - self:SetGoalTotal() -- Polymorphic to set the initial goal total! - - if self.Dispatcher then - self:E( "Firing Assign event " ) - self.Dispatcher:Assign( self, PlayerUnit, PlayerName ) - end - - self:GetMission():__Start( 1 ) - - -- When the task is assigned, the task goal needs to be checked of the derived classes. - self:__Goal( -10 ) -- Polymorphic - - self:SetMenu() - end -end - - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onenterSuccess( From, Event, To ) - - self:E( "Task Success" ) - - self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " is successful! Good job!" ) - self:UnAssignFromGroups() - - self:GetMission():__MissionGoals( 1 ) - -end - - ---- FSM function for a TASK --- @param #TASK self --- @param #string From --- @param #string Event --- @param #string To -function TASK:onenterAborted( From, Event, To ) - - self:E( "Task Aborted" ) - - if From ~= "Aborted" then - self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has been aborted! Task may be replanned." ) - self:__Replan( 5 ) - self:SetMenu() - end - -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string From --- @param #string Event --- @param #string To -function TASK:onenterCancelled( From, Event, To ) - - self:E( "Task Cancelled" ) - - if From ~= "Cancelled" then - self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has been cancelled! The tactical situation has changed." ) - self:UnAssignFromGroups() - self:SetMenu() - end - -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string From --- @param #string Event --- @param #string To -function TASK:onafterReplan( From, Event, To ) - - self:E( "Task Replanned" ) - - self:GetMission():GetCommandCenter():MessageToCoalition( "Replanning Task " .. self:GetName() .. "." ) - - self:SetMenu() - -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string From --- @param #string Event --- @param #string To -function TASK:onenterFailed( From, Event, To ) - - self:E( "Task Failed" ) - - self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has failed!" ) - - self:UnAssignFromGroups() -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onstatechange( From, Event, To ) - - if self:IsTrace() then - --MESSAGE:New( "@ Task " .. self.TaskName .. " : " .. From .. " changed to " .. To .. " by " .. Event, 2 ):ToAll() - end - - if self.Scores[To] then - local Scoring = self:GetScoring() - if Scoring then - self:E( { self.Scores[To].ScoreText, self.Scores[To].Score } ) - Scoring:_AddMissionScore( self.Mission, self.Scores[To].ScoreText, self.Scores[To].Score ) - end - end - -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onenterPlanned( From, Event, To) - if not self.TimeOut == 0 then - self.__TimeOut( self.TimeOut ) - end -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onbeforeTimeOut( From, Event, To ) - if From == "Planned" then - self:RemoveMenu() - return true - end - return false -end - -do -- Links - - --- Set dispatcher of a task - -- @param #TASK self - -- @param Tasking.DetectionManager#DETECTION_MANAGER Dispatcher - -- @return #TASK - function TASK:SetDispatcher( Dispatcher ) - self.Dispatcher = Dispatcher - end - - --- Set detection of a task - -- @param #TASK self - -- @param Function.Detection#DETECTION_BASE Detection - -- @param #number DetectedItemIndex - -- @return #TASK - function TASK:SetDetection( Detection, DetectedItemIndex ) - - self:E({DetectedItemIndex,Detection}) - - self.Detection = Detection - self.DetectedItemIndex = DetectedItemIndex - end - -end - -do -- Reporting - ---- Create a summary report of the Task. --- List the Task Name and Status --- @param #TASK self --- @param Wrapper.Group#GROUP ReportGroup --- @return #string -function TASK:ReportSummary( ReportGroup ) - - local Report = REPORT:New() - - -- List the name of the Task. - Report:Add( self:GetName() ) - - -- Determine the status of the Task. - Report:Add( "State: <" .. self:GetState() .. ">" ) - - if self.TaskInfo["Coordinates"] then - local TaskInfoIDText = string.format( "%s: ", "Coordinate" ) - local TaskCoord = self.TaskInfo["Coordinates"].TaskInfoText -- Core.Point#COORDINATE - Report:Add( TaskInfoIDText .. TaskCoord:ToString( ReportGroup, nil, self ) ) - end - - return Report:Text( ', ' ) -end - ---- Create an overiew report of the Task. --- List the Task Name and Status --- @param #TASK self --- @return #string -function TASK:ReportOverview( ReportGroup ) - - self:UpdateTaskInfo() - - -- List the name of the Task. - local TaskName = self:GetName() - local Report = REPORT:New() - - local Line = 0 - local LineReport = REPORT:New() - - for TaskInfoID, TaskInfo in UTILS.spairs( self.TaskInfo, function( t, a, b ) return t[a].TaskInfoOrder < t[b].TaskInfoOrder end ) do - - self:F( { TaskInfo = TaskInfo } ) - - if Line < math.floor( TaskInfo.TaskInfoOrder / 10 ) then - if Line ~= 0 then - Report:AddIndent( LineReport:Text( ", " ) ) - else - Report:Add( TaskName .. ", " .. LineReport:Text( ", " ) ) - end - LineReport = REPORT:New() - Line = math.floor( TaskInfo.TaskInfoOrder / 10 ) - end - - local TaskInfoIDText = string.format( "%s: ", TaskInfoID ) - - if type( TaskInfo.TaskInfoText ) == "string" then - LineReport:Add( TaskInfoIDText .. TaskInfo.TaskInfoText ) - elseif type(TaskInfo) == "table" then - if TaskInfoID == "Coordinates" then - local ToCoordinate = TaskInfo.TaskInfoText -- Core.Point#COORDINATE - --Report:Add( TaskInfoIDText ) - LineReport:Add( TaskInfoIDText .. ToCoordinate:ToString( ReportGroup, nil, self ) ) - --Report:AddIndent( ToCoordinate:ToStringBULLS( ReportGroup:GetCoalition() ) ) - else - end - end - end - - Report:AddIndent( LineReport:Text( ", " ) ) - - return Report:Text() -end - ---- Create a count of the players in the Task. --- @param #TASK self --- @return #number The total number of players in the task. -function TASK:GetPlayerCount() --R2.1 Get a count of the players. - - local PlayerCount = 0 - - -- Loop each Unit active in the Task, and find Player Names. - for TaskGroupID, PlayerGroup in pairs( self:GetGroups():GetSet() ) do - local PlayerGroup = PlayerGroup -- Wrapper.Group#GROUP - if self:IsGroupAssigned( PlayerGroup ) then - local PlayerNames = PlayerGroup:GetPlayerNames() - PlayerCount = PlayerCount + #PlayerNames - end - end - - return PlayerCount -end - - ---- Create a list of the players in the Task. --- @param #TASK self --- @return #map<#string,Wrapper.Group#GROUP> A map of the players -function TASK:GetPlayerNames() --R2.1 Get a map of the players. - - local PlayerNameMap = {} - - -- Loop each Unit active in the Task, and find Player Names. - for TaskGroupID, PlayerGroup in pairs( self:GetGroups():GetSet() ) do - local PlayerGroup = PlayerGroup -- Wrapper.Group#GROUP - if self:IsGroupAssigned( PlayerGroup ) then - local PlayerNames = PlayerGroup:GetPlayerNames() - for PlayerNameID, PlayerName in pairs( PlayerNames ) do - PlayerNameMap[PlayerName] = PlayerGroup - end - end - end - - return PlayerNameMap -end - - ---- Create a detailed report of the Task. --- List the Task Status, and the Players assigned to the Task. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @return #string -function TASK:ReportDetails( ReportGroup ) - - self:UpdateTaskInfo() - - local Report = REPORT:New():SetIndent( 3 ) - - -- List the name of the Task. - local Name = self:GetName() - - -- Determine the status of the Task. - local Status = "<" .. self:GetState() .. ">" - - Report:Add( "Task: " .. Name .. " - " .. Status .. " - Detailed Report" ) - - -- Loop each Unit active in the Task, and find Player Names. - local PlayerNames = self:GetPlayerNames() - - local PlayerReport = REPORT:New() - for PlayerName, PlayerGroup in pairs( PlayerNames ) do - PlayerReport:Add( "Group " .. PlayerGroup:GetCallsign() .. ": " .. PlayerName ) - end - local Players = PlayerReport:Text() - - if Players ~= "" then - Report:Add( " - Players assigned:" ) - Report:AddIndent( Players ) - end - - for TaskInfoID, TaskInfo in pairs( self.TaskInfo, function( t, a, b ) return t[a].TaskInfoOrder < t[b].TaskInfoOrder end ) do - - local TaskInfoIDText = string.format( " - %s: ", TaskInfoID ) - - if type( TaskInfo.TaskInfoText ) == "string" then - Report:Add( TaskInfoIDText .. TaskInfo.TaskInfoText ) - elseif type(TaskInfo) == "table" then - if TaskInfoID == "Coordinates" then - local FromCoordinate = ReportGroup:GetUnit(1):GetCoordinate() - local ToCoordinate = TaskInfo.TaskInfoText -- Core.Point#COORDINATE - Report:Add( TaskInfoIDText .. ToCoordinate:ToString( ReportGroup:GetUnit(1), nil, self ) ) - else - end - end - - end - - local Coordinate = self:GetInfo( "Coordinates" ) -- Core.Point#COORDINATE - - local Velocity = self.TargetSetUnit:GetVelocity() - local Heading = self.TargetSetUnit:GetHeading() - - Coordinate:SetHeading( Heading ) - Coordinate:SetVelocity( Velocity ) - - Report:Add( "Targets are" .. Coordinate:GetMovingText() .. "." ) - - return Report:Text() -end - - -end -- Reporting - - -do -- Additional Task Scoring and Task Progress - - --- Add Task Progress for a Player Name - -- @param #TASK self - -- @param #string PlayerName The name of the player. - -- @param #string ProgressText The text that explains the Progress achieved. - -- @param #number ProgressTime The time the progress was achieved. - -- @oaram #number ProgressPoints The amount of points of magnitude granted. This will determine the shared Mission Success scoring. - -- @return #TASK - function TASK:AddProgress( PlayerName, ProgressText, ProgressTime, ProgressPoints ) - self.TaskProgress = self.TaskProgress or {} - self.TaskProgress[ProgressTime] = self.TaskProgress[ProgressTime] or {} - self.TaskProgress[ProgressTime].PlayerName = PlayerName - self.TaskProgress[ProgressTime].ProgressText = ProgressText - self.TaskProgress[ProgressTime].ProgressPoints = ProgressPoints - self:GetMission():AddPlayerName( PlayerName ) - return self - end - - function TASK:GetPlayerProgress( PlayerName ) - local ProgressPlayer = 0 - for ProgressTime, ProgressData in pairs( self.TaskProgress ) do - if PlayerName == ProgressData.PlayerName then - ProgressPlayer = ProgressPlayer + ProgressData.ProgressPoints - end - end - return ProgressPlayer - end - - --- Set a score when progress has been made by the player. - -- @param #TASK self - -- @param #string PlayerName The name of the player. - -- @param #number Score The score in points to be granted when task process has been achieved. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK - function TASK:SetScoreOnProgress( PlayerName, Score, TaskUnit ) - self:F( { PlayerName, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountPlayer", "Player " .. PlayerName .. " has achieved progress.", Score ) - - return self - end - - --- Set a score when all the targets in scope of the A2A attack, have been destroyed. - -- @param #TASK self - -- @param #string PlayerName The name of the player. - -- @param #number Score The score in points. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK - function TASK:SetScoreOnSuccess( PlayerName, Score, TaskUnit ) - self:F( { PlayerName, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScore( "Success", "The task is a success!", Score ) - - return self - end - - --- Set a penalty when the A2A attack has failed. - -- @param #TASK self - -- @param #string PlayerName The name of the player. - -- @param #number Penalty The penalty in points, must be a negative value! - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK - function TASK:SetScoreOnFail( PlayerName, Penalty, TaskUnit ) - self:F( { PlayerName, Penalty, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScore( "Failed", "The task is a failure!", Penalty ) - - return self - end - -end ---- This module contains the DETECTION_MANAGER class and derived classes. --- --- === --- --- 1) @{DetectionManager#DETECTION_MANAGER} class, extends @{Fsm#FSM} --- ==================================================================== --- The @{DetectionManager#DETECTION_MANAGER} class defines the core functions to report detected objects to groups. --- Reportings can be done in several manners, and it is up to the derived classes if DETECTION_MANAGER to model the reporting behaviour. --- --- 1.1) DETECTION_MANAGER constructor: --- ----------------------------------- --- * @{DetectionManager#DETECTION_MANAGER.New}(): Create a new DETECTION_MANAGER instance. --- --- 1.2) DETECTION_MANAGER reporting: --- --------------------------------- --- Derived DETECTION_MANAGER classes will reports detected units using the method @{DetectionManager#DETECTION_MANAGER.ReportDetected}(). This method implements polymorphic behaviour. --- --- The time interval in seconds of the reporting can be changed using the methods @{DetectionManager#DETECTION_MANAGER.SetRefreshTimeInterval}(). --- To control how long a reporting message is displayed, use @{DetectionManager#DETECTION_MANAGER.SetReportDisplayTime}(). --- Derived classes need to implement the method @{DetectionManager#DETECTION_MANAGER.GetReportDisplayTime}() to use the correct display time for displayed messages during a report. --- --- Reporting can be started and stopped using the methods @{DetectionManager#DETECTION_MANAGER.StartReporting}() and @{DetectionManager#DETECTION_MANAGER.StopReporting}() respectively. --- If an ad-hoc report is requested, use the method @{DetectionManager#DETECTION_MANAGER#ReportNow}(). --- --- The default reporting interval is every 60 seconds. The reporting messages are displayed 15 seconds. --- --- === --- --- 2) @{DetectionManager#DETECTION_REPORTING} class, extends @{DetectionManager#DETECTION_MANAGER} --- ========================================================================================= --- The @{DetectionManager#DETECTION_REPORTING} class implements detected units reporting. Reporting can be controlled using the reporting methods available in the @{DetectionManager#DETECTION_MANAGER} class. --- --- 2.1) DETECTION_REPORTING constructor: --- ------------------------------- --- The @{DetectionManager#DETECTION_REPORTING.New}() method creates a new DETECTION_REPORTING instance. --- --- --- === --- --- ### Contributions: Mechanist, Prof_Hilactic, FlightControl - Concept & Testing --- ### Author: FlightControl - Framework Design & Programming --- --- @module DetectionManager - -do -- DETECTION MANAGER - - --- DETECTION_MANAGER class. - -- @type DETECTION_MANAGER - -- @field Set#SET_GROUP SetGroup The groups to which the FAC will report to. - -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. - -- @extends Core.Fsm#FSM - DETECTION_MANAGER = { - ClassName = "DETECTION_MANAGER", - SetGroup = nil, - Detection = nil, - } - - --- FAC constructor. - -- @param #DETECTION_MANAGER self - -- @param Set#SET_GROUP SetGroup - -- @param Functional.Detection#DETECTION_BASE Detection - -- @return #DETECTION_MANAGER self - function DETECTION_MANAGER:New( SetGroup, Detection ) - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM:New() ) -- #DETECTION_MANAGER - - self.SetGroup = SetGroup - self.Detection = Detection - - self:SetStartState( "Stopped" ) - self:AddTransition( "Stopped", "Start", "Started" ) - - --- Start Handler OnBefore for DETECTION_MANAGER - -- @function [parent=#DETECTION_MANAGER] OnBeforeStart - -- @param #DETECTION_MANAGER self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- Start Handler OnAfter for DETECTION_MANAGER - -- @function [parent=#DETECTION_MANAGER] OnAfterStart - -- @param #DETECTION_MANAGER self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- Start Trigger for DETECTION_MANAGER - -- @function [parent=#DETECTION_MANAGER] Start - -- @param #DETECTION_MANAGER self - - --- Start Asynchronous Trigger for DETECTION_MANAGER - -- @function [parent=#DETECTION_MANAGER] __Start - -- @param #DETECTION_MANAGER self - -- @param #number Delay - - - - self:AddTransition( "Started", "Stop", "Stopped" ) - - --- Stop Handler OnBefore for DETECTION_MANAGER - -- @function [parent=#DETECTION_MANAGER] OnBeforeStop - -- @param #DETECTION_MANAGER self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- Stop Handler OnAfter for DETECTION_MANAGER - -- @function [parent=#DETECTION_MANAGER] OnAfterStop - -- @param #DETECTION_MANAGER self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- Stop Trigger for DETECTION_MANAGER - -- @function [parent=#DETECTION_MANAGER] Stop - -- @param #DETECTION_MANAGER self - - --- Stop Asynchronous Trigger for DETECTION_MANAGER - -- @function [parent=#DETECTION_MANAGER] __Stop - -- @param #DETECTION_MANAGER self - -- @param #number Delay - - - self:AddTransition( "Started", "Report", "Started" ) - - self:SetRefreshTimeInterval( 30 ) - self:SetReportDisplayTime( 25 ) - - self:E( { Detection = Detection } ) - Detection:__Start( 3 ) - - return self - end - - function DETECTION_MANAGER:onafterStart( From, Event, To ) - self:Report() - end - - function DETECTION_MANAGER:onafterReport( From, Event, To ) - - self:E( "onafterReport" ) - - self:__Report( -self._RefreshTimeInterval ) - - self:ProcessDetected( self.Detection ) - end - - --- Set the reporting time interval. - -- @param #DETECTION_MANAGER self - -- @param #number RefreshTimeInterval The interval in seconds when a report needs to be done. - -- @return #DETECTION_MANAGER self - function DETECTION_MANAGER:SetRefreshTimeInterval( RefreshTimeInterval ) - self:F2() - - self._RefreshTimeInterval = RefreshTimeInterval - end - - - --- Set the reporting message display time. - -- @param #DETECTION_MANAGER self - -- @param #number ReportDisplayTime The display time in seconds when a report needs to be done. - -- @return #DETECTION_MANAGER self - function DETECTION_MANAGER:SetReportDisplayTime( ReportDisplayTime ) - self:F2() - - self._ReportDisplayTime = ReportDisplayTime - end - - --- Get the reporting message display time. - -- @param #DETECTION_MANAGER self - -- @return #number ReportDisplayTime The display time in seconds when a report needs to be done. - function DETECTION_MANAGER:GetReportDisplayTime() - self:F2() - - return self._ReportDisplayTime - end - - --- Reports the detected items to the @{Set#SET_GROUP}. - -- @param #DETECTION_MANAGER self - -- @param Functional.Detection#DETECTION_BASE Detection - -- @return #DETECTION_MANAGER self - function DETECTION_MANAGER:ProcessDetected( Detection ) - self:E() - - end - -end - - -do -- DETECTION_REPORTING - - --- DETECTION_REPORTING class. - -- @type DETECTION_REPORTING - -- @field Set#SET_GROUP SetGroup The groups to which the FAC will report to. - -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. - -- @extends #DETECTION_MANAGER - DETECTION_REPORTING = { - ClassName = "DETECTION_REPORTING", - } - - - --- DETECTION_REPORTING constructor. - -- @param #DETECTION_REPORTING self - -- @param Set#SET_GROUP SetGroup - -- @param Functional.Detection#DETECTION_AREAS Detection - -- @return #DETECTION_REPORTING self - function DETECTION_REPORTING:New( SetGroup, Detection ) - - -- Inherits from DETECTION_MANAGER - local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #DETECTION_REPORTING - - self:Schedule( 1, 30 ) - return self - end - - --- Creates a string of the detected items in a @{Detection}. - -- @param #DETECTION_MANAGER self - -- @param Set#SET_UNIT DetectedSet The detected Set created by the @{Detection#DETECTION_BASE} object. - -- @return #DETECTION_MANAGER self - function DETECTION_REPORTING:GetDetectedItemsText( DetectedSet ) - self:F2() - - local MT = {} -- Message Text - local UnitTypes = {} - - for DetectedUnitID, DetectedUnitData in pairs( DetectedSet:GetSet() ) do - local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT - if DetectedUnit:IsAlive() then - local UnitType = DetectedUnit:GetTypeName() - - if not UnitTypes[UnitType] then - UnitTypes[UnitType] = 1 - else - UnitTypes[UnitType] = UnitTypes[UnitType] + 1 - end - end - end - - for UnitTypeID, UnitType in pairs( UnitTypes ) do - MT[#MT+1] = UnitType .. " of " .. UnitTypeID - end - - return table.concat( MT, ", " ) - end - - - - --- Reports the detected items to the @{Set#SET_GROUP}. - -- @param #DETECTION_REPORTING self - -- @param Wrapper.Group#GROUP Group The @{Group} object to where the report needs to go. - -- @param Functional.Detection#DETECTION_AREAS Detection The detection created by the @{Detection#DETECTION_BASE} object. - -- @return #boolean Return true if you want the reporting to continue... false will cancel the reporting loop. - function DETECTION_REPORTING:ProcessDetected( Group, Detection ) - self:F2( Group ) - - self:E( Group ) - local DetectedMsg = {} - for DetectedAreaID, DetectedAreaData in pairs( Detection:GetDetectedAreas() ) do - local DetectedArea = DetectedAreaData -- Functional.Detection#DETECTION_AREAS.DetectedArea - DetectedMsg[#DetectedMsg+1] = " - Group #" .. DetectedAreaID .. ": " .. self:GetDetectedItemsText( DetectedArea.Set ) - end - local FACGroup = Detection:GetDetectionGroups() - FACGroup:MessageToGroup( "Reporting detected target groups:\n" .. table.concat( DetectedMsg, "\n" ), self:GetReportDisplayTime(), Group ) - - return true - end - -end - ---- **Tasking** - The TASK_A2G_DISPATCHER creates and manages player TASK_A2G tasks based on detected targets. --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- ==== --- --- @module Task_A2G_Dispatcher - -do -- TASK_A2G_DISPATCHER - - --- TASK_A2G_DISPATCHER class. - -- @type TASK_A2G_DISPATCHER - -- @field Set#SET_GROUP SetGroup The groups to which the FAC will report to. - -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. - -- @field Tasking.Mission#MISSION Mission - -- @extends Tasking.DetectionManager#DETECTION_MANAGER - - --- # TASK_A2G_DISPATCHE} class, extends @{#DETECTION_MANAGER} - -- - -- The TASK_A2G_DISPATCHER class implements the dynamic dispatching of tasks upon groups of detected units determined a @{Set} of FAC (groups). - -- The FAC will detect units, will group them, and will dispatch @{Task}s to groups. Depending on the type of target detected, different tasks will be dispatched. - -- Find a summary below describing for which situation a task type is created: - -- - -- * **CAS Task**: Is created when there are enemy ground units within range of the FAC, while there are friendly units in the FAC perimeter. - -- * **BAI Task**: Is created when there are enemy ground units within range of the FAC, while there are NO other friendly units within the FAC perimeter. - -- * **SEAD Task**: Is created when there are enemy ground units wihtin range of the FAC, with air search radars. - -- - -- Other task types will follow... - -- - -- ## TASK_A2G_DISPATCHER constructor - -- - -- The @{#TASK_A2G_DISPATCHER.New}() method creates a new TASK_A2G_DISPATCHER instance. - -- - -- @field #TASK_A2G_DISPATCHER - TASK_A2G_DISPATCHER = { - ClassName = "TASK_A2G_DISPATCHER", - Mission = nil, - Detection = nil, - Tasks = {}, - } - - - --- TASK_A2G_DISPATCHER constructor. - -- @param #TASK_A2G_DISPATCHER self - -- @param Tasking.Mission#MISSION Mission The mission for which the task dispatching is done. - -- @param Set#SET_GROUP SetGroup The set of groups that can join the tasks within the mission. - -- @param Functional.Detection#DETECTION_BASE Detection The detection results that are used to dynamically assign new tasks to human players. - -- @return #TASK_A2G_DISPATCHER self - function TASK_A2G_DISPATCHER:New( Mission, SetGroup, Detection ) - - -- Inherits from DETECTION_MANAGER - local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #TASK_A2G_DISPATCHER - - self.Detection = Detection - self.Mission = Mission - - self.Detection:FilterCategories( Unit.Category.GROUND_UNIT, Unit.Category.SHIP ) - self.Detection:FilterFriendliesCategory( Unit.Category.GROUND_UNIT ) - - self:AddTransition( "Started", "Assign", "Started" ) - - --- OnAfter Transition Handler for Event Assign. - -- @function [parent=#TASK_A2G_DISPATCHER] OnAfterAssign - -- @param #TASK_A2G_DISPATCHER self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param Tasking.Task_A2G#TASK_A2G Task - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param #string PlayerName - - self:__Start( 5 ) - - return self - end - - - --- Creates a SEAD task when there are targets for it. - -- @param #TASK_A2G_DISPATCHER self - -- @param Functional.Detection#DETECTION_AREAS.DetectedItem DetectedItem - -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units. - -- @return #nil If there are no targets to be set. - function TASK_A2G_DISPATCHER:EvaluateSEAD( DetectedItem ) - self:F( { DetectedItem.ItemID } ) - - local DetectedSet = DetectedItem.Set - local DetectedZone = DetectedItem.Zone - - -- Determine if the set has radar targets. If it does, construct a SEAD task. - local RadarCount = DetectedSet:HasSEAD() - - if RadarCount > 0 then - - -- Here we're doing something advanced... We're copying the DetectedSet, but making a new Set only with SEADable Radar units in it. - local TargetSetUnit = SET_UNIT:New() - TargetSetUnit:SetDatabase( DetectedSet ) - TargetSetUnit:FilterHasSEAD() - TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - - return TargetSetUnit - end - - return nil - end - - --- Creates a CAS task when there are targets for it. - -- @param #TASK_A2G_DISPATCHER self - -- @param Functional.Detection#DETECTION_AREAS.DetectedItem DetectedItem - -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units. - -- @return #nil If there are no targets to be set. - function TASK_A2G_DISPATCHER:EvaluateCAS( DetectedItem ) - self:F( { DetectedItem.ItemID } ) - - local DetectedSet = DetectedItem.Set - local DetectedZone = DetectedItem.Zone - - - -- Determine if the set has radar targets. If it does, construct a SEAD task. - local GroundUnitCount = DetectedSet:HasGroundUnits() - local FriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem ) - local RadarCount = DetectedSet:HasSEAD() - - if RadarCount == 0 and GroundUnitCount > 0 and FriendliesNearBy == true then - - -- Copy the Set - local TargetSetUnit = SET_UNIT:New() - TargetSetUnit:SetDatabase( DetectedSet ) - TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - - return TargetSetUnit - end - - return nil - end - - --- Creates a BAI task when there are targets for it. - -- @param #TASK_A2G_DISPATCHER self - -- @param Functional.Detection#DETECTION_AREAS.DetectedItem DetectedItem - -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units. - -- @return #nil If there are no targets to be set. - function TASK_A2G_DISPATCHER:EvaluateBAI( DetectedItem, FriendlyCoalition ) - self:F( { DetectedItem.ItemID } ) - - local DetectedSet = DetectedItem.Set - local DetectedZone = DetectedItem.Zone - - - -- Determine if the set has radar targets. If it does, construct a SEAD task. - local GroundUnitCount = DetectedSet:HasGroundUnits() - local FriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem ) - local RadarCount = DetectedSet:HasSEAD() - - if RadarCount == 0 and GroundUnitCount > 0 and FriendliesNearBy == false then - - -- Copy the Set - local TargetSetUnit = SET_UNIT:New() - TargetSetUnit:SetDatabase( DetectedSet ) - TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - - return TargetSetUnit - end - - return nil - end - - - function TASK_A2G_DISPATCHER:RemoveTask( TaskIndex ) - self.Mission:RemoveTask( self.Tasks[TaskIndex] ) - self.Tasks[TaskIndex] = nil - end - - --- Evaluates the removal of the Task from the Mission. - -- Can only occur when the DetectedItem is Changed AND the state of the Task is "Planned". - -- @param #TASK_A2G_DISPATCHER self - -- @param Tasking.Mission#MISSION Mission - -- @param Tasking.Task#TASK Task - -- @param #boolean DetectedItemID - -- @param #boolean DetectedItemChange - -- @return Tasking.Task#TASK - function TASK_A2G_DISPATCHER:EvaluateRemoveTask( Mission, Task, TaskIndex, DetectedItemChanged ) - - if Task then - if ( Task:IsStatePlanned() and DetectedItemChanged == true ) or Task:IsStateCancelled() then - --self:E( "Removing Tasking: " .. Task:GetTaskName() ) - self:RemoveTask( TaskIndex ) - end - end - - return Task - end - - - --- Assigns tasks in relation to the detected items to the @{Set#SET_GROUP}. - -- @param #TASK_A2G_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Detection#DETECTION_BASE} derived object. - -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. - function TASK_A2G_DISPATCHER:ProcessDetected( Detection ) - self:E() - - local AreaMsg = {} - local TaskMsg = {} - local ChangeMsg = {} - - local Mission = self.Mission - - if Mission:IsIDLE() or Mission:IsENGAGED() then - - local TaskReport = REPORT:New() - - -- Checking the task queue for the dispatcher, and removing any obsolete task! - for TaskIndex, TaskData in pairs( self.Tasks ) do - local Task = TaskData -- Tasking.Task#TASK - if Task:IsStatePlanned() then - local DetectedItem = Detection:GetDetectedItem( TaskIndex ) - if not DetectedItem then - local TaskText = Task:GetName() - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - Mission:GetCommandCenter():MessageToGroup( string.format( "Obsolete A2G task %s for %s removed.", TaskText, Mission:GetName() ), TaskGroup ) - end - Task = self:RemoveTask( TaskIndex ) - Mission:RemoveTask( Task ) - self.Tasks[TaskIndex] = nil - end - end - end - - --- First we need to the detected targets. - for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do - - local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem - local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT - local DetectedZone = DetectedItem.Zone - --self:E( { "Targets in DetectedItem", DetectedItem.ItemID, DetectedSet:Count(), tostring( DetectedItem ) } ) - --DetectedSet:Flush() - - local DetectedItemID = DetectedItem.ID - local TaskIndex = DetectedItem.Index - local DetectedItemChanged = DetectedItem.Changed - - self:E( { DetectedItemChanged = DetectedItemChanged, DetectedItemID = DetectedItemID, TaskIndex = TaskIndex } ) - - local Task = self.Tasks[TaskIndex] -- Tasking.Task_A2G#TASK_A2G - - if Task then - -- If there is a Task and the task was assigned, then we check if the task was changed ... If it was, we need to reevaluate the targets. - if Task:IsStateAssigned() then - if DetectedItemChanged == true then -- The detection has changed, thus a new TargetSet is to be evaluated and set - local TargetsReport = REPORT:New() - local TargetSetUnit = self:EvaluateSEAD( DetectedItem ) -- Returns a SetUnit if there are targets to be SEADed... - if TargetSetUnit then - if Task:IsInstanceOf( TASK_A2G_SEAD ) then - Task:SetTargetSetUnit( TargetSetUnit ) - Task:UpdateTaskInfo() - TargetsReport:Add( Detection:GetChangeText( DetectedItem ) ) - else - Task:Cancel() - end - else - local TargetSetUnit = self:EvaluateCAS( DetectedItem ) -- Returns a SetUnit if there are targets to be CASed... - if TargetSetUnit then - if Task:IsInstanceOf( TASK_A2G_CAS ) then - Task:SetTargetSetUnit( TargetSetUnit ) - Task:SetDetection( Detection, TaskIndex ) - Task:UpdateTaskInfo() - TargetsReport:Add( Detection:GetChangeText( DetectedItem ) ) - else - Task:Cancel() - Task = self:RemoveTask( TaskIndex ) - end - else - local TargetSetUnit = self:EvaluateBAI( DetectedItem ) -- Returns a SetUnit if there are targets to be BAIed... - if TargetSetUnit then - if Task:IsInstanceOf( TASK_A2G_BAI ) then - Task:SetTargetSetUnit( TargetSetUnit ) - Task:SetDetection( Detection, TaskIndex ) - Task:UpdateTaskInfo() - TargetsReport:Add( Detection:GetChangeText( DetectedItem ) ) - else - Task:Cancel() - Task = self:RemoveTask( TaskIndex ) - end - end - end - end - - -- Now we send to each group the changes, if any. - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - local TargetsText = TargetsReport:Text(", ") - if ( Mission:IsGroupAssigned(TaskGroup) ) and TargetsText ~= "" then - Mission:GetCommandCenter():MessageToGroup( string.format( "Task %s has change of targets:\n %s", Task:GetName(), TargetsText ), TaskGroup ) - end - end - end - end - end - - if Task then - if Task:IsStatePlanned() then - if DetectedItemChanged == true then -- The detection has changed, thus a new TargetSet is to be evaluated and set - if Task:IsInstanceOf( TASK_A2G_SEAD ) then - local TargetSetUnit = self:EvaluateSEAD( DetectedItem ) -- Returns a SetUnit if there are targets to be SEADed... - if TargetSetUnit then - Task:SetTargetSetUnit( TargetSetUnit ) - Task:UpdateTaskInfo() - else - Task:Cancel() - Task = self:RemoveTask( TaskIndex ) - end - else - if Task:IsInstanceOf( TASK_A2G_CAS ) then - local TargetSetUnit = self:EvaluateCAS( DetectedItem ) -- Returns a SetUnit if there are targets to be CASed... - if TargetSetUnit then - Task:SetTargetSetUnit( TargetSetUnit ) - Task:SetDetection( Detection, TaskIndex ) - Task:UpdateTaskInfo() - else - Task:Cancel() - Task = self:RemoveTask( TaskIndex ) - end - else - if Task:IsInstanceOf( TASK_A2G_BAI ) then - local TargetSetUnit = self:EvaluateBAI( DetectedItem ) -- Returns a SetUnit if there are targets to be BAIed... - if TargetSetUnit then - Task:SetTargetSetUnit( TargetSetUnit ) - Task:SetDetection( Detection, TaskIndex ) - Task:UpdateTaskInfo() - else - Task:Cancel() - Task = self:RemoveTask( TaskIndex ) - end - else - Task:Cancel() - Task = self:RemoveTask( TaskIndex ) - end - end - end - end - end - end - - -- Evaluate SEAD - if not Task then - local TargetSetUnit = self:EvaluateSEAD( DetectedItem ) -- Returns a SetUnit if there are targets to be SEADed... - if TargetSetUnit then - Task = TASK_A2G_SEAD:New( Mission, self.SetGroup, string.format( "SEAD.%03d", DetectedItemID ), TargetSetUnit ) - Task:SetDetection( Detection, TaskIndex ) - end - - -- Evaluate CAS - if not Task then - local TargetSetUnit = self:EvaluateCAS( DetectedItem ) -- Returns a SetUnit if there are targets to be CASed... - if TargetSetUnit then - Task = TASK_A2G_CAS:New( Mission, self.SetGroup, string.format( "CAS.%03d", DetectedItemID ), TargetSetUnit ) - Task:SetDetection( Detection, TaskIndex ) - end - - -- Evaluate BAI - if not Task then - local TargetSetUnit = self:EvaluateBAI( DetectedItem, self.Mission:GetCommandCenter():GetPositionable():GetCoalition() ) -- Returns a SetUnit if there are targets to be BAIed... - if TargetSetUnit then - Task = TASK_A2G_BAI:New( Mission, self.SetGroup, string.format( "BAI.%03d", DetectedItemID ), TargetSetUnit ) - Task:SetDetection( Detection, TaskIndex ) - end - end - end - - if Task then - self.Tasks[TaskIndex] = Task - Task:SetTargetZone( DetectedZone ) - Task:SetDispatcher( self ) - Task:UpdateTaskInfo() - Mission:AddTask( Task ) - - TaskReport:Add( Task:GetName() ) - else - self:E("This should not happen") - end - end - - - -- OK, so the tasking has been done, now delete the changes reported for the area. - Detection:AcceptChanges( DetectedItem ) - end - - -- TODO set menus using the HQ coordinator - Mission:GetCommandCenter():SetMenu() - - local TaskText = TaskReport:Text(", ") - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" then - Mission:GetCommandCenter():MessageToGroup( string.format( "%s has tasks %s. Subscribe to a task using the radio menu.", Mission:GetName(), TaskText ), TaskGroup ) - end - end - - end - - return true - end - -end--- **Tasking** - The TASK_A2G models tasks for players in Air to Ground engagements. --- --- ![Banner Image](..\Presentations\TASK_A2G\Dia1.JPG) --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- ==== --- --- @module Task_A2G - -do -- TASK_A2G - - --- The TASK_A2G class - -- @type TASK_A2G - -- @field Set#SET_UNIT TargetSetUnit - -- @extends Tasking.Task#TASK - - --- # TASK_A2G class, extends @{Task#TASK} - -- - -- The TASK_A2G class defines Air To Ground tasks for a @{Set} of Target Units, - -- based on the tasking capabilities defined in @{Task#TASK}. - -- The TASK_A2G is implemented using a @{Fsm#FSM_TASK}, and has the following statuses: - -- - -- * **None**: Start of the process - -- * **Planned**: The A2G task is planned. - -- * **Assigned**: The A2G task is assigned to a @{Group#GROUP}. - -- * **Success**: The A2G task is successfully completed. - -- * **Failed**: The A2G task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. - -- - -- ## Set the scoring of achievements in an A2G attack. - -- - -- Scoring or penalties can be given in the following circumstances: - -- - -- * @{#TASK_A2G.SetScoreOnDestroy}(): Set a score when a target in scope of the A2G attack, has been destroyed. - -- * @{#TASK_A2G.SetScoreOnSuccess}(): Set a score when all the targets in scope of the A2G attack, have been destroyed. - -- * @{#TASK_A2G.SetPenaltyOnFailed}(): Set a penalty when the A2G attack has failed. - -- - -- @field #TASK_A2G - TASK_A2G = { - ClassName = "TASK_A2G", - } - - --- Instantiates a new TASK_A2G. - -- @param #TASK_A2G self - -- @param Tasking.Mission#MISSION Mission - -- @param Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. - -- @param #string TaskName The name of the Task. - -- @param Set#SET_UNIT UnitSetTargets - -- @param #number TargetDistance The distance to Target when the Player is considered to have "arrived" at the engagement range. - -- @param Core.Zone#ZONE_BASE TargetZone The target zone, if known. - -- If the TargetZone parameter is specified, the player will be routed to the center of the zone where all the targets are assumed to be. - -- @return #TASK_A2G self - function TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskType, TaskBriefing ) - local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType, TaskBriefing ) ) -- Tasking.Task#TASK_A2G - self:F() - - self.TargetSetUnit = TargetSetUnit - self.TaskType = TaskType - - local Fsm = self:GetUnitProcess() - - - Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "RouteToRendezVous", Rejected = "Reject" } ) - - Fsm:AddTransition( "Assigned", "RouteToRendezVous", "RoutingToRendezVous" ) - Fsm:AddProcess ( "RoutingToRendezVous", "RouteToRendezVousPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtRendezVous" } ) - Fsm:AddProcess ( "RoutingToRendezVous", "RouteToRendezVousZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtRendezVous" } ) - - Fsm:AddTransition( { "Arrived", "RoutingToRendezVous" }, "ArriveAtRendezVous", "ArrivedAtRendezVous" ) - - Fsm:AddTransition( { "ArrivedAtRendezVous", "HoldingAtRendezVous" }, "Engage", "Engaging" ) - Fsm:AddTransition( { "ArrivedAtRendezVous", "HoldingAtRendezVous" }, "HoldAtRendezVous", "HoldingAtRendezVous" ) - - Fsm:AddProcess ( "Engaging", "Account", ACT_ACCOUNT_DEADS:New(), {} ) - Fsm:AddTransition( "Engaging", "RouteToTarget", "Engaging" ) - Fsm:AddProcess( "Engaging", "RouteToTargetZone", ACT_ROUTE_ZONE:New(), {} ) - Fsm:AddProcess( "Engaging", "RouteToTargetPoint", ACT_ROUTE_POINT:New(), {} ) - Fsm:AddTransition( "Engaging", "RouteToTargets", "Engaging" ) - - --Fsm:AddTransition( "Accounted", "DestroyedAll", "Accounted" ) - --Fsm:AddTransition( "Accounted", "Success", "Success" ) - Fsm:AddTransition( "Rejected", "Reject", "Aborted" ) - Fsm:AddTransition( "Failed", "Fail", "Failed" ) - - - --- Test - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_A2G#TASK_A2G Task - function Fsm:onafterRouteToRendezVous( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - -- Determine the first Unit from the self.RendezVousSetUnit - - if Task:GetRendezVousZone( TaskUnit ) then - self:__RouteToRendezVousZone( 0.1 ) - else - if Task:GetRendezVousCoordinate( TaskUnit ) then - self:__RouteToRendezVousPoint( 0.1 ) - else - self:__ArriveAtRendezVous( 0.1 ) - end - end - end - - --- Test - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task#TASK_A2G Task - function Fsm:OnAfterArriveAtRendezVous( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - -- Determine the first Unit from the self.TargetSetUnit - - self:__Engage( 0.1 ) - end - - --- Test - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task#TASK_A2G Task - function Fsm:onafterEngage( TaskUnit, Task ) - self:E( { self } ) - self:__Account( 0.1 ) - self:__RouteToTarget(0.1 ) - self:__RouteToTargets( -10 ) - end - - --- Test - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_A2G#TASK_A2G Task - function Fsm:onafterRouteToTarget( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - -- Determine the first Unit from the self.TargetSetUnit - - if Task:GetTargetZone( TaskUnit ) then - self:__RouteToTargetZone( 0.1 ) - else - local TargetUnit = Task.TargetSetUnit:GetFirst() -- Wrapper.Unit#UNIT - if TargetUnit then - local Coordinate = TargetUnit:GetPointVec3() - self:T( { TargetCoordinate = Coordinate, Coordinate:GetX(), Coordinate:GetY(), Coordinate:GetZ() } ) - Task:SetTargetCoordinate( Coordinate, TaskUnit ) - end - self:__RouteToTargetPoint( 0.1 ) - end - end - - --- Test - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_A2G#TASK_A2G Task - function Fsm:onafterRouteToTargets( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - local TargetUnit = Task.TargetSetUnit:GetFirst() -- Wrapper.Unit#UNIT - if TargetUnit then - Task:SetTargetCoordinate( TargetUnit:GetCoordinate(), TaskUnit ) - end - self:__RouteToTargets( -10 ) - end - - return self - - end - - --- @param #TASK_A2G self - -- @param Core.Set#SET_UNIT TargetSetUnit The set of targets. - function TASK_A2G:SetTargetSetUnit( TargetSetUnit ) - - self.TargetSetUnit = TargetSetUnit - end - - - - --- @param #TASK_A2G self - function TASK_A2G:GetPlannedMenuText() - return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" - end - - --- @param #TASK_A2G self - -- @param Core.Point#COORDINATE RendezVousCoordinate The Coordinate object referencing to the 2D point where the RendezVous point is located on the map. - -- @param #number RendezVousRange The RendezVousRange that defines when the player is considered to have arrived at the RendezVous point. - -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_A2G:SetRendezVousCoordinate( RendezVousCoordinate, RendezVousRange, TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT - ActRouteRendezVous:SetCoordinate( RendezVousCoordinate ) - ActRouteRendezVous:SetRange( RendezVousRange ) - end - - --- @param #TASK_A2G self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return Core.Point#COORDINATE The Coordinate object referencing to the 2D point where the RendezVous point is located on the map. - -- @return #number The RendezVousRange that defines when the player is considered to have arrived at the RendezVous point. - function TASK_A2G:GetRendezVousCoordinate( TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT - return ActRouteRendezVous:GetCoordinate(), ActRouteRendezVous:GetRange() - end - - - - --- @param #TASK_A2G self - -- @param Core.Zone#ZONE_BASE RendezVousZone The Zone object where the RendezVous is located on the map. - -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_A2G:SetRendezVousZone( RendezVousZone, TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE - ActRouteRendezVous:SetZone( RendezVousZone ) - end - - --- @param #TASK_A2G self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return Core.Zone#ZONE_BASE The Zone object where the RendezVous is located on the map. - function TASK_A2G:GetRendezVousZone( TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE - return ActRouteRendezVous:GetZone() - end - - --- @param #TASK_A2G self - -- @param Core.Point#COORDINATE TargetCoordinate The Coordinate object where the Target is located on the map. - -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_A2G:SetTargetCoordinate( TargetCoordinate, TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT - ActRouteTarget:SetCoordinate( TargetCoordinate ) - end - - - --- @param #TASK_A2G self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return Core.Point#COORDINATE The Coordinate object where the Target is located on the map. - function TASK_A2G:GetTargetCoordinate( TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT - return ActRouteTarget:GetCoordinate() - end - - - --- @param #TASK_A2G self - -- @param Core.Zone#ZONE_BASE TargetZone The Zone object where the Target is located on the map. - -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_A2G:SetTargetZone( TargetZone, TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE - ActRouteTarget:SetZone( TargetZone ) - end - - - --- @param #TASK_A2G self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return Core.Zone#ZONE_BASE The Zone object where the Target is located on the map. - function TASK_A2G:GetTargetZone( TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE - return ActRouteTarget:GetZone() - end - - function TASK_A2G:SetGoalTotal() - - self.GoalTotal = self.TargetSetUnit:Count() - end - - function TASK_A2G:GetGoalTotal() - - return self.GoalTotal - end - -end - - -do -- TASK_A2G_SEAD - - --- The TASK_A2G_SEAD class - -- @type TASK_A2G_SEAD - -- @field Set#SET_UNIT TargetSetUnit - -- @extends Tasking.Task#TASK - - --- # TASK_A2G_SEAD class, extends @{Task_A2G#TASK_A2G} - -- - -- The TASK_A2G_SEAD class defines an Suppression or Extermination of Air Defenses task for a human player to be executed. - -- These tasks are important to be executed as they will help to achieve air superiority at the vicinity. - -- - -- The TASK_A2G_SEAD is used by the @{Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create SEAD tasks - -- based on detected enemy ground targets. - -- - -- @field #TASK_A2G_SEAD - TASK_A2G_SEAD = { - ClassName = "TASK_A2G_SEAD", - } - - --- Instantiates a new TASK_A2G_SEAD. - -- @param #TASK_A2G_SEAD self - -- @param Tasking.Mission#MISSION Mission - -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. - -- @param #string TaskName The name of the Task. - -- @param Core.Set#SET_UNIT TargetSetUnit - -- @param #string TaskBriefing The briefing of the task. - -- @return #TASK_A2G_SEAD self - function TASK_A2G_SEAD:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing) - local self = BASE:Inherit( self, TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, "SEAD", TaskBriefing ) ) -- #TASK_A2G_SEAD - self:F() - - Mission:AddTask( self ) - - self:SetBriefing( - TaskBriefing or - "Execute a Suppression of Enemy Air Defenses." - ) - - return self - end - - function TASK_A2G_SEAD:UpdateTaskInfo() - - - local TargetCoordinate = self.Detection and self.Detection:GetDetectedItemCoordinate( self.DetectedItemIndex ) or self.TargetSetUnit:GetFirst():GetCoordinate() - self:SetInfo( "Coordinates", TargetCoordinate, 0 ) - - local ThreatLevel, ThreatText - if self.Detection then - ThreatLevel, ThreatText = self.Detection:GetDetectedItemThreatLevel( self.DetectedItemIndex ) - else - ThreatLevel, ThreatText = self.TargetSetUnit:CalculateThreatLevelA2G() - end - self:SetInfo( "Threat", ThreatText .. " [" .. string.rep( "■", ThreatLevel ) .. "]", 11 ) - - if self.Detection then - local DetectedItemsCount = self.TargetSetUnit:Count() - local ReportTypes = REPORT:New() - local TargetTypes = {} - for TargetUnitName, TargetUnit in pairs( self.TargetSetUnit:GetSet() ) do - local TargetType = self.Detection:GetDetectedUnitTypeName( TargetUnit ) - if not TargetTypes[TargetType] then - TargetTypes[TargetType] = TargetType - ReportTypes:Add( TargetType ) - end - end - self:SetInfo( "Targets", string.format( "%d of %s", DetectedItemsCount, ReportTypes:Text( ", " ) ), 10 ) - else - local DetectedItemsCount = self.TargetSetUnit:Count() - local DetectedItemsTypes = self.TargetSetUnit:GetTypeNames() - self:SetInfo( "Targets", string.format( "%d of %s", DetectedItemsCount, DetectedItemsTypes ), 10 ) - end - - end - - function TASK_A2G_SEAD:ReportOrder( ReportGroup ) - local Coordinate = self:GetInfo( "Coordinates" ) - --local Coordinate = self.TaskInfo.Coordinates.TaskInfoText - local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate ) - - return Distance - end - - - --- @param #TASK_A2G_SEAD self - function TASK_A2G_SEAD:onafterGoal( TaskUnit, From, Event, To ) - local TargetSetUnit = self.TargetSetUnit -- Core.Set#SET_UNIT - - if TargetSetUnit:Count() == 0 then - self:Success() - end - - self:__Goal( -10 ) - end - - --- Set a score when a target in scope of the A2G attack, has been destroyed . - -- @param #TASK_A2G_SEAD self - -- @param #string PlayerName The name of the player. - -- @param #number Score The score in points to be granted when task process has been achieved. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2G_SEAD - function TASK_A2G_SEAD:SetScoreOnProgress( PlayerName, Score, TaskUnit ) - self:F( { PlayerName, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has SEADed a target.", Score ) - - return self - end - - --- Set a score when all the targets in scope of the A2G attack, have been destroyed. - -- @param #TASK_A2G_SEAD self - -- @param #string PlayerName The name of the player. - -- @param #number Score The score in points. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2G_SEAD - function TASK_A2G_SEAD:SetScoreOnSuccess( PlayerName, Score, TaskUnit ) - self:F( { PlayerName, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScore( "Success", "All radar emitting targets have been successfully SEADed!", Score ) - - return self - end - - --- Set a penalty when the A2G attack has failed. - -- @param #TASK_A2G_SEAD self - -- @param #string PlayerName The name of the player. - -- @param #number Penalty The penalty in points, must be a negative value! - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2G_SEAD - function TASK_A2G_SEAD:SetScoreOnFail( PlayerName, Penalty, TaskUnit ) - self:F( { PlayerName, Penalty, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScore( "Failed", "The SEADing has failed!", Penalty ) - - return self - end - - -end - -do -- TASK_A2G_BAI - - --- The TASK_A2G_BAI class - -- @type TASK_A2G_BAI - -- @field Set#SET_UNIT TargetSetUnit - -- @extends Tasking.Task#TASK - - --- # TASK_A2G_BAI class, extends @{Task_A2G#TASK_A2G} - -- - -- The TASK_A2G_BAI class defines an Battlefield Air Interdiction task for a human player to be executed. - -- These tasks are more strategic in nature and are most of the time further away from friendly forces. - -- BAI tasks can also be used to express the abscence of friendly forces near the vicinity. - -- - -- The TASK_A2G_BAI is used by the @{Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create BAI tasks - -- based on detected enemy ground targets. - -- - -- @field #TASK_A2G_BAI - TASK_A2G_BAI = { - ClassName = "TASK_A2G_BAI", - } - - --- Instantiates a new TASK_A2G_BAI. - -- @param #TASK_A2G_BAI self - -- @param Tasking.Mission#MISSION Mission - -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. - -- @param #string TaskName The name of the Task. - -- @param Core.Set#SET_UNIT TargetSetUnit - -- @param #string TaskBriefing The briefing of the task. - -- @return #TASK_A2G_BAI self - function TASK_A2G_BAI:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing ) - local self = BASE:Inherit( self, TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, "BAI", TaskBriefing ) ) -- #TASK_A2G_BAI - self:F() - - Mission:AddTask( self ) - - self:SetBriefing( - TaskBriefing or - "Execute a Battlefield Air Interdiction of a group of enemy targets." - ) - - return self - end - - function TASK_A2G_BAI:UpdateTaskInfo() - - self:E({self.Detection, self.DetectedItemIndex}) - - local TargetCoordinate = self.Detection and self.Detection:GetDetectedItemCoordinate( self.DetectedItemIndex ) or self.TargetSetUnit:GetFirst():GetCoordinate() - self:SetInfo( "Coordinates", TargetCoordinate, 0 ) - - local ThreatLevel, ThreatText - if self.Detection then - ThreatLevel, ThreatText = self.Detection:GetDetectedItemThreatLevel( self.DetectedItemIndex ) - else - ThreatLevel, ThreatText = self.TargetSetUnit:CalculateThreatLevelA2G() - end - self:SetInfo( "Threat", ThreatText .. " [" .. string.rep( "■", ThreatLevel ) .. "]", 11 ) - - if self.Detection then - local DetectedItemsCount = self.TargetSetUnit:Count() - local ReportTypes = REPORT:New() - local TargetTypes = {} - for TargetUnitName, TargetUnit in pairs( self.TargetSetUnit:GetSet() ) do - local TargetType = self.Detection:GetDetectedUnitTypeName( TargetUnit ) - if not TargetTypes[TargetType] then - TargetTypes[TargetType] = TargetType - ReportTypes:Add( TargetType ) - end - end - self:SetInfo( "Targets", string.format( "%d of %s", DetectedItemsCount, ReportTypes:Text( ", " ) ), 10 ) - else - local DetectedItemsCount = self.TargetSetUnit:Count() - local DetectedItemsTypes = self.TargetSetUnit:GetTypeNames() - self:SetInfo( "Targets", string.format( "%d of %s", DetectedItemsCount, DetectedItemsTypes ), 10 ) - end - - end - - - function TASK_A2G_BAI:ReportOrder( ReportGroup ) - local Coordinate = self:GetInfo( "Coordinates" ) - --local Coordinate = self.TaskInfo.Coordinates.TaskInfoText - local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate ) - - return Distance - end - - - --- @param #TASK_A2G_BAI self - function TASK_A2G_BAI:onafterGoal( TaskUnit, From, Event, To ) - local TargetSetUnit = self.TargetSetUnit -- Core.Set#SET_UNIT - - if TargetSetUnit:Count() == 0 then - self:Success() - end - - self:__Goal( -10 ) - end - - --- Set a score when a target in scope of the A2G attack, has been destroyed . - -- @param #TASK_A2G_BAI self - -- @param #string PlayerName The name of the player. - -- @param #number Score The score in points to be granted when task process has been achieved. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2G_BAI - function TASK_A2G_BAI:SetScoreOnProgress( PlayerName, Score, TaskUnit ) - self:F( { PlayerName, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has destroyed a target in Battlefield Air Interdiction (BAI).", Score ) - - return self - end - - --- Set a score when all the targets in scope of the A2G attack, have been destroyed. - -- @param #TASK_A2G_BAI self - -- @param #string PlayerName The name of the player. - -- @param #number Score The score in points. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2G_BAI - function TASK_A2G_BAI:SetScoreOnSuccess( PlayerName, Score, TaskUnit ) - self:F( { PlayerName, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScore( "Success", "All targets have been successfully destroyed! The Battlefield Air Interdiction (BAI) is a success!", Score ) - - return self - end - - --- Set a penalty when the A2G attack has failed. - -- @param #TASK_A2G_BAI self - -- @param #string PlayerName The name of the player. - -- @param #number Penalty The penalty in points, must be a negative value! - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2G_BAI - function TASK_A2G_BAI:SetScoreOnFail( PlayerName, Penalty, TaskUnit ) - self:F( { PlayerName, Penalty, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScore( "Failed", "The Battlefield Air Interdiction (BAI) has failed!", Penalty ) - - return self - end - - -end - -do -- TASK_A2G_CAS - - --- The TASK_A2G_CAS class - -- @type TASK_A2G_CAS - -- @field Set#SET_UNIT TargetSetUnit - -- @extends Tasking.Task#TASK - - --- # TASK_A2G_CAS class, extends @{Task_A2G#TASK_A2G} - -- - -- The TASK_A2G_CAS class defines an Close Air Support task for a human player to be executed. - -- Friendly forces will be in the vicinity within 6km from the enemy. - -- - -- The TASK_A2G_CAS is used by the @{Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create CAS tasks - -- based on detected enemy ground targets. - -- - -- @field #TASK_A2G_CAS - TASK_A2G_CAS = { - ClassName = "TASK_A2G_CAS", - } - - --- Instantiates a new TASK_A2G_CAS. - -- @param #TASK_A2G_CAS self - -- @param Tasking.Mission#MISSION Mission - -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. - -- @param #string TaskName The name of the Task. - -- @param Core.Set#SET_UNIT TargetSetUnit - -- @param #string TaskBriefing The briefing of the task. - -- @return #TASK_A2G_CAS self - function TASK_A2G_CAS:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing ) - local self = BASE:Inherit( self, TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, "CAS", TaskBriefing ) ) -- #TASK_A2G_CAS - self:F() - - Mission:AddTask( self ) - - self:SetBriefing( - TaskBriefing or - "Execute a Close Air Support for a group of enemy targets. " .. - "Beware of friendlies at the vicinity! " - ) - - - return self - end - - function TASK_A2G_CAS:UpdateTaskInfo() - - local TargetCoordinate = ( self.Detection and self.Detection:GetDetectedItemCoordinate( self.DetectedItemIndex ) ) or self.TargetSetUnit:GetFirst():GetCoordinate() - self:SetInfo( "Coordinates", TargetCoordinate, 0 ) - - local ThreatLevel, ThreatText - if self.Detection then - ThreatLevel, ThreatText = self.Detection:GetDetectedItemThreatLevel( self.DetectedItemIndex ) - else - ThreatLevel, ThreatText = self.TargetSetUnit:CalculateThreatLevelA2G() - end - self:SetInfo( "Threat", ThreatText .. " [" .. string.rep( "■", ThreatLevel ) .. "]", 11 ) - - if self.Detection then - local DetectedItemsCount = self.TargetSetUnit:Count() - local ReportTypes = REPORT:New() - local TargetTypes = {} - for TargetUnitName, TargetUnit in pairs( self.TargetSetUnit:GetSet() ) do - local TargetType = self.Detection:GetDetectedUnitTypeName( TargetUnit ) - if not TargetTypes[TargetType] then - TargetTypes[TargetType] = TargetType - ReportTypes:Add( TargetType ) - end - end - self:SetInfo( "Targets", string.format( "%d of %s", DetectedItemsCount, ReportTypes:Text( ", " ) ), 10 ) - else - local DetectedItemsCount = self.TargetSetUnit:Count() - local DetectedItemsTypes = self.TargetSetUnit:GetTypeNames() - self:SetInfo( "Targets", string.format( "%d of %s", DetectedItemsCount, DetectedItemsTypes ), 10 ) - end - - end - - --- @param #TASK_A2G_CAS self - function TASK_A2G_CAS:ReportOrder( ReportGroup ) - - local Coordinate = self:GetInfo( "Coordinates" ) - local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate ) - - return Distance - end - - - --- @param #TASK_A2G_CAS self - function TASK_A2G_CAS:onafterGoal( TaskUnit, From, Event, To ) - local TargetSetUnit = self.TargetSetUnit -- Core.Set#SET_UNIT - - if TargetSetUnit:Count() == 0 then - self:Success() - end - - self:__Goal( -10 ) - end - - --- Set a score when a target in scope of the A2G attack, has been destroyed . - -- @param #TASK_A2G_CAS self - -- @param #string PlayerName The name of the player. - -- @param #number Score The score in points to be granted when task process has been achieved. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2G_CAS - function TASK_A2G_CAS:SetScoreOnProgress( PlayerName, Score, TaskUnit ) - self:F( { PlayerName, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has destroyed a target in Close Air Support (CAS).", Score ) - - return self - end - - --- Set a score when all the targets in scope of the A2G attack, have been destroyed. - -- @param #TASK_A2G_CAS self - -- @param #string PlayerName The name of the player. - -- @param #number Score The score in points. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2G_CAS - function TASK_A2G_CAS:SetScoreOnSuccess( PlayerName, Score, TaskUnit ) - self:F( { PlayerName, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScore( "Success", "All targets have been successfully destroyed! The Close Air Support (CAS) was a success!", Score ) - - return self - end - - --- Set a penalty when the A2G attack has failed. - -- @param #TASK_A2G_CAS self - -- @param #string PlayerName The name of the player. - -- @param #number Penalty The penalty in points, must be a negative value! - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2G_CAS - function TASK_A2G_CAS:SetScoreOnFail( PlayerName, Penalty, TaskUnit ) - self:F( { PlayerName, Penalty, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScore( "Failed", "The Close Air Support (CAS) has failed!", Penalty ) - - return self - end - - -end ---- **Tasking** - The TASK_A2A_DISPATCHER creates and manages player TASK_A2A tasks based on detected targets. --- --- The @{#TASK_A2A_DISPATCHER} classes implement the dynamic dispatching of tasks upon groups of detected units determined a @{Set} of EWR installation groups. --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- ==== --- --- @module Task_A2A_Dispatcher - -do -- TASK_A2A_DISPATCHER - - --- TASK_A2A_DISPATCHER class. - -- @type TASK_A2A_DISPATCHER - -- @extends Tasking.DetectionManager#DETECTION_MANAGER - - --- # TASK_A2A_DISPATCHER class, extends @{Tasking#DETECTION_MANAGER} - -- - -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia1.JPG) - -- - -- The @{#TASK_A2A_DISPATCHER} class implements the dynamic dispatching of tasks upon groups of detected units determined a @{Set} of EWR installation groups. - -- - -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia3.JPG) - -- - -- The EWR will detect units, will group them, and will dispatch @{Task}s to groups. Depending on the type of target detected, different tasks will be dispatched. - -- Find a summary below describing for which situation a task type is created: - -- - -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia9.JPG) - -- - -- * **INTERCEPT Task**: Is created when the target is known, is detected and within a danger zone, and there is no friendly airborne in range. - -- * **SWEEP Task**: Is created when the target is unknown, was detected and the last position is only known, and within a danger zone, and there is no friendly airborne in range. - -- * **ENGAGE Task**: Is created when the target is known, is detected and within a danger zone, and there is a friendly airborne in range, that will receive this task. - -- - -- ## 1. TASK\_A2A\_DISPATCHER constructor: - -- - -- The @{#TASK_A2A_DISPATCHER.New}() method creates a new TASK\_A2A\_DISPATCHER instance. - -- - -- ### 1.1. Define or set the **Mission**: - -- - -- Tasking is executed to accomplish missions. Therefore, a MISSION object needs to be given as the first parameter. - -- - -- local HQ = GROUP:FindByName( "HQ", "Bravo" ) - -- local CommandCenter = COMMANDCENTER:New( HQ, "Lima" ) - -- local Mission = MISSION:New( CommandCenter, "A2A Mission", "High", "Watch the air enemy units being detected.", coalition.side.RED ) - -- - -- Missions are governed by COMMANDCENTERS, so, ensure you have a COMMANDCENTER object installed and setup within your mission. - -- Create the MISSION object, and hook it under the command center. - -- - -- ### 1.2. Build a set of the groups seated by human players: - -- - -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia6.JPG) - -- - -- A set or collection of the groups wherein human players can be seated, these can be clients or units that can be joined as a slot or jumping into. - -- - -- local AttackGroups = SET_GROUP:New():FilterCoalitions( "red" ):FilterPrefixes( "Defender" ):FilterStart() - -- - -- The set is built using the SET_GROUP class. Apply any filter criteria to identify the correct groups for your mission. - -- Only these slots or units will be able to execute the mission and will receive tasks for this mission, once available. - -- - -- ### 1.3. Define the **EWR network**: - -- - -- As part of the TASK\_A2A\_DISPATCHER constructor, an EWR network must be given as the third parameter. - -- An EWR network, or, Early Warning Radar network, is used to early detect potential airborne targets and to understand the position of patrolling targets of the enemy. - -- - -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia5.JPG) - -- - -- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. - -- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US). - -- Additionally, ANY other radar capable unit can be part of the EWR network! Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar. - -- The position of these units is very important as they need to provide enough coverage - -- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them. - -- - -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia7.JPG) - -- - -- Additionally in a hot war situation where the border is no longer respected the placement of radars has a big effect on how fast the war escalates. - -- For example if they are a long way forward and can detect enemy planes on the ground and taking off - -- they will start to vector CAP and GCI flights to attack them straight away which will immediately draw a response from the other coalition. - -- Having the radars further back will mean a slower escalation because fewer targets will be detected and - -- therefore less CAP and GCI flights will spawn and this will tend to make just the border area active rather than a melee over the whole map. - -- It all depends on what the desired effect is. - -- - -- EWR networks are **dynamically constructed**, that is, they form part of the @{Functional#DETECTION_BASE} object that is given as the input parameter of the TASK\_A2A\_DISPATCHER class. - -- By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network, - -- increasing or decreasing the radar coverage of the Early Warning System. - -- - -- See the following example to setup an EWR network containing EWR stations and AWACS. - -- - -- local EWRSet = SET_GROUP:New():FilterPrefixes( "EWR" ):FilterCoalitions("red"):FilterStart() - -- - -- local EWRDetection = DETECTION_AREAS:New( EWRSet, 6000 ) - -- EWRDetection:SetFriendliesRange( 10000 ) - -- EWRDetection:SetRefreshTimeInterval(30) - -- - -- -- Setup the A2A dispatcher, and initialize it. - -- A2ADispatcher = TASK_A2A_DISPATCHER:New( Mission, AttackGroups, EWRDetection ) - -- - -- The above example creates a SET_GROUP instance, and stores this in the variable (object) **EWRSet**. - -- **EWRSet** is then being configured to filter all active groups with a group name starting with **EWR** to be included in the Set. - -- **EWRSet** is then being ordered to start the dynamic filtering. Note that any destroy or new spawn of a group with the above names will be removed or added to the Set. - -- Then a new **EWRDetection** object is created from the class DETECTION_AREAS. A grouping radius of 6000 is choosen, which is 6km. - -- The **EWRDetection** object is then passed to the @{#TASK_A2A_DISPATCHER.New}() method to indicate the EWR network configuration and setup the A2A tasking and detection mechanism. - -- - -- ### 2. Define the detected **target grouping radius**: - -- - -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia8.JPG) - -- - -- The target grouping radius is a property of the Detection object, that was passed to the AI\_A2A\_DISPATCHER object, but can be changed. - -- The grouping radius should not be too small, but also depends on the types of planes and the era of the simulation. - -- Fast planes like in the 80s, need a larger radius than WWII planes. - -- Typically I suggest to use 30000 for new generation planes and 10000 for older era aircraft. - -- - -- Note that detected targets are constantly re-grouped, that is, when certain detected aircraft are moving further than the group radius, then these aircraft will become a separate - -- group being detected. This may result in additional GCI being started by the dispatcher! So don't make this value too small! - -- - -- ## 3. Set the **Engage radius**: - -- - -- Define the radius to engage any target by airborne friendlies, which are executing cap or returning from an intercept mission. - -- - -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia11.JPG) - -- - -- So, if there is a target area detected and reported, - -- then any friendlies that are airborne near this target area, - -- will be commanded to (re-)engage that target when available (if no other tasks were commanded). - -- For example, if 100000 is given as a value, then any friendly that is airborne within 100km from the detected target, - -- will be considered to receive the command to engage that target area. - -- You need to evaluate the value of this parameter carefully. - -- If too small, more intercept missions may be triggered upon detected target areas. - -- If too large, any airborne cap may not be able to reach the detected target area in time, because it is too far. - -- - -- ## 4. Set **Scoring** and **Messages**: - -- - -- The TASK\_A2A\_DISPATCHER is a state machine. It triggers the event Assign when a new player joins a @{Task} dispatched by the TASK\_A2A\_DISPATCHER. - -- An _event handler_ can be defined to catch the **Assign** event, and add **additional processing** to set _scoring_ and to _define messages_, - -- when the player reaches certain achievements in the task. - -- - -- The prototype to handle the **Assign** event needs to be developed as follows: - -- - -- TaskDispatcher = TASK_A2A_DISPATCHER:New( ... ) - -- - -- --- @param #TaskDispatcher self - -- -- @param #string From Contains the name of the state from where the Event was triggered. - -- -- @param #string Event Contains the name of the event that was triggered. In this case Assign. - -- -- @param #string To Contains the name of the state that will be transitioned to. - -- -- @param Tasking.Task_A2A#TASK_A2A Task The Task object, which is any derived object from TASK_A2A. - -- -- @param Wrapper.Unit#UNIT TaskUnit The Unit or Client that contains the Player. - -- -- @param #string PlayerName The name of the Player that joined the TaskUnit. - -- function TaskDispatcher:OnAfterAssign( From, Event, To, Task, TaskUnit, PlayerName ) - -- Task:SetScoreOnProgress( PlayerName, 20, TaskUnit ) - -- Task:SetScoreOnSuccess( PlayerName, 200, TaskUnit ) - -- Task:SetScoreOnFail( PlayerName, -100, TaskUnit ) - -- end - -- - -- The **OnAfterAssign** method (function) is added to the TaskDispatcher object. - -- This method will be called when a new player joins a unit in the set of groups in scope of the dispatcher. - -- So, this method will be called only **ONCE** when a player joins a unit in scope of the task. - -- - -- The TASK class implements various methods to additional **set scoring** for player achievements: - -- - -- * @{Tasking.Task#TASK.SetScoreOnProgress}() will add additional scores when a player achieves **Progress** while executing the task. - -- Examples of **task progress** can be destroying units, arriving at zones etc. - -- - -- * @{Tasking.Task#TASK.SetScoreOnSuccess}() will add additional scores when the task goes into **Success** state. - -- This means the **task has been successfully completed**. - -- - -- * @{Tasking.Task#TASK.SetScoreOnSuccess}() will add additional (negative) scores when the task goes into **Failed** state. - -- This means the **task has not been successfully completed**, and the scores must be given with a negative value! - -- - -- @field #TASK_A2A_DISPATCHER - TASK_A2A_DISPATCHER = { - ClassName = "TASK_A2A_DISPATCHER", - Mission = nil, - Detection = nil, - Tasks = {}, - SweepZones = {}, - } - - - --- TASK_A2A_DISPATCHER constructor. - -- @param #TASK_A2A_DISPATCHER self - -- @param Tasking.Mission#MISSION Mission The mission for which the task dispatching is done. - -- @param Set#SET_GROUP SetGroup The set of groups that can join the tasks within the mission. - -- @param Functional.Detection#DETECTION_BASE Detection The detection results that are used to dynamically assign new tasks to human players. - -- @return #TASK_A2A_DISPATCHER self - function TASK_A2A_DISPATCHER:New( Mission, SetGroup, Detection ) - - -- Inherits from DETECTION_MANAGER - local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #TASK_A2A_DISPATCHER - - self.Detection = Detection - self.Mission = Mission - - - -- TODO: Check detection through radar. - self.Detection:FilterCategories( Unit.Category.AIRPLANE, Unit.Category.HELICOPTER ) - self.Detection:InitDetectRadar( true ) - self.Detection:SetRefreshTimeInterval( 30 ) - - self:AddTransition( "Started", "Assign", "Started" ) - - --- OnAfter Transition Handler for Event Assign. - -- @function [parent=#TASK_A2A_DISPATCHER] OnAfterAssign - -- @param #TASK_A2A_DISPATCHER self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param Tasking.Task_A2A#TASK_A2A Task - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param #string PlayerName - - self:__Start( 5 ) - - return self - end - - - --- Define the radius to when an ENGAGE task will be generated for any nearby by airborne friendlies, which are executing cap or returning from an intercept mission. - -- So, if there is a target area detected and reported, - -- then any friendlies that are airborne near this target area, - -- will be commanded to (re-)engage that target when available (if no other tasks were commanded). - -- An ENGAGE task will be created for those pilots. - -- For example, if 100000 is given as a value, then any friendly that is airborne within 100km from the detected target, - -- will be considered to receive the command to engage that target area. - -- You need to evaluate the value of this parameter carefully. - -- If too small, more intercept missions may be triggered upon detected target areas. - -- If too large, any airborne cap may not be able to reach the detected target area in time, because it is too far. - -- @param #TASK_A2A_DISPATCHER self - -- @param #number EngageRadius (Optional, Default = 100000) The radius to report friendlies near the target. - -- @return #TASK_A2A_DISPATCHER - -- @usage - -- - -- -- Set 50km as the radius to engage any target by airborne friendlies. - -- TaskA2ADispatcher:SetEngageRadius( 50000 ) - -- - -- -- Set 100km as the radius to engage any target by airborne friendlies. - -- TaskA2ADispatcher:SetEngageRadius() -- 100000 is the default value. - -- - function TASK_A2A_DISPATCHER:SetEngageRadius( EngageRadius ) - - self.Detection:SetFriendliesRange( EngageRadius or 100000 ) - - return self - end - - - - --- Creates an INTERCEPT task when there are targets for it. - -- @param #TASK_A2A_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem - -- @return Set#SET_UNIT TargetSetUnit: The target set of units. - -- @return #nil If there are no targets to be set. - function TASK_A2A_DISPATCHER:EvaluateINTERCEPT( DetectedItem ) - self:F( { DetectedItem.ItemID } ) - - local DetectedSet = DetectedItem.Set - local DetectedZone = DetectedItem.Zone - - -- Check if there is at least one UNIT in the DetectedSet is visible. - - if DetectedItem.IsDetected == true then - - -- Here we're doing something advanced... We're copying the DetectedSet. - local TargetSetUnit = SET_UNIT:New() - TargetSetUnit:SetDatabase( DetectedSet ) - TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - - return TargetSetUnit - end - - return nil - end - - - --- Creates an SWEEP task when there are targets for it. - -- @param #TASK_A2A_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem - -- @return Set#SET_UNIT TargetSetUnit: The target set of units. - -- @return #nil If there are no targets to be set. - function TASK_A2A_DISPATCHER:EvaluateSWEEP( DetectedItem ) - self:F( { DetectedItem.ItemID } ) - - local DetectedSet = DetectedItem.Set - local DetectedZone = DetectedItem.Zone - - - if DetectedItem.IsDetected == false then - - -- Here we're doing something advanced... We're copying the DetectedSet. - local TargetSetUnit = SET_UNIT:New() - TargetSetUnit:SetDatabase( DetectedSet ) - TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - - return TargetSetUnit - end - - return nil - end - - - --- Creates an ENGAGE task when there are human friendlies airborne near the targets. - -- @param #TASK_A2A_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem - -- @return Set#SET_UNIT TargetSetUnit: The target set of units. - -- @return #nil If there are no targets to be set. - function TASK_A2A_DISPATCHER:EvaluateENGAGE( DetectedItem ) - self:F( { DetectedItem.ItemID } ) - - local DetectedSet = DetectedItem.Set - local DetectedZone = DetectedItem.Zone - - local PlayersCount, PlayersReport = self:GetPlayerFriendliesNearBy( DetectedItem ) - - - -- Only allow ENGAGE when there are Players near the zone, and when the Area has detected items since the last run in a 60 seconds time zone. - if PlayersCount > 0 and DetectedItem.IsDetected == true then - - -- Here we're doing something advanced... We're copying the DetectedSet. - local TargetSetUnit = SET_UNIT:New() - TargetSetUnit:SetDatabase( DetectedSet ) - TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - - return TargetSetUnit - end - - return nil - end - - - - - --- Evaluates the removal of the Task from the Mission. - -- Can only occur when the DetectedItem is Changed AND the state of the Task is "Planned". - -- @param #TASK_A2A_DISPATCHER self - -- @param Tasking.Mission#MISSION Mission - -- @param Tasking.Task#TASK Task - -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Detection#DETECTION_BASE} derived object. - -- @param #boolean DetectedItemID - -- @param #boolean DetectedItemChange - -- @return Tasking.Task#TASK - function TASK_A2A_DISPATCHER:EvaluateRemoveTask( Mission, Task, Detection, DetectedItem, DetectedItemIndex, DetectedItemChanged ) - - if Task then - - if Task:IsStatePlanned() then - local TaskName = Task:GetName() - local TaskType = TaskName:match( "(%u+)%.%d+" ) - - self:T2( { TaskType = TaskType } ) - - local Remove = false - - local IsPlayers = Detection:IsPlayersNearBy( DetectedItem ) - if TaskType == "ENGAGE" then - if IsPlayers == false then - Remove = true - end - end - - if TaskType == "INTERCEPT" then - if IsPlayers == true then - Remove = true - end - if DetectedItem.IsDetected == false then - Remove = true - end - end - - if TaskType == "SWEEP" then - if DetectedItem.IsDetected == true then - Remove = true - end - end - - local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT - --DetectedSet:Flush() - --self:E( { DetectedSetCount = DetectedSet:Count() } ) - if DetectedSet:Count() == 0 then - Remove = true - end - - if DetectedItemChanged == true or Remove then - Task = self:RemoveTask( DetectedItemIndex ) - end - end - end - - return Task - end - - --- Calculates which friendlies are nearby the area - -- @param #TASK_A2A_DISPATCHER self - -- @param DetectedItem - -- @return #number, Core.CommandCenter#REPORT - function TASK_A2A_DISPATCHER:GetFriendliesNearBy( DetectedItem ) - - local DetectedSet = DetectedItem.Set - local FriendlyUnitsNearBy = self.Detection:GetFriendliesNearBy( DetectedItem ) - - local FriendlyTypes = {} - local FriendliesCount = 0 - - if FriendlyUnitsNearBy then - local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() - for FriendlyUnitName, FriendlyUnitData in pairs( FriendlyUnitsNearBy ) do - local FriendlyUnit = FriendlyUnitData -- Wrapper.Unit#UNIT - if FriendlyUnit:IsAirPlane() then - local FriendlyUnitThreatLevel = FriendlyUnit:GetThreatLevel() - FriendliesCount = FriendliesCount + 1 - local FriendlyType = FriendlyUnit:GetTypeName() - FriendlyTypes[FriendlyType] = FriendlyTypes[FriendlyType] and ( FriendlyTypes[FriendlyType] + 1 ) or 1 - if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then - end - end - end - - end - - --self:E( { FriendliesCount = FriendliesCount } ) - - local FriendlyTypesReport = REPORT:New() - - if FriendliesCount > 0 then - for FriendlyType, FriendlyTypeCount in pairs( FriendlyTypes ) do - FriendlyTypesReport:Add( string.format("%d of %s", FriendlyTypeCount, FriendlyType ) ) - end - else - FriendlyTypesReport:Add( "-" ) - end - - - return FriendliesCount, FriendlyTypesReport - end - - --- Calculates which HUMAN friendlies are nearby the area - -- @param #TASK_A2A_DISPATCHER self - -- @param DetectedItem - -- @return #number, Core.CommandCenter#REPORT - function TASK_A2A_DISPATCHER:GetPlayerFriendliesNearBy( DetectedItem ) - - local DetectedSet = DetectedItem.Set - local PlayersNearBy = self.Detection:GetPlayersNearBy( DetectedItem ) - - local PlayerTypes = {} - local PlayersCount = 0 - - if PlayersNearBy then - local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() - for PlayerUnitName, PlayerUnitData in pairs( PlayersNearBy ) do - local PlayerUnit = PlayerUnitData -- Wrapper.Unit#UNIT - local PlayerName = PlayerUnit:GetPlayerName() - --self:E( { PlayerName = PlayerName, PlayerUnit = PlayerUnit } ) - if PlayerUnit:IsAirPlane() and PlayerName ~= nil then - local FriendlyUnitThreatLevel = PlayerUnit:GetThreatLevel() - PlayersCount = PlayersCount + 1 - local PlayerType = PlayerUnit:GetTypeName() - PlayerTypes[PlayerName] = PlayerType - if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then - end - end - end - - end - - --self:E( { PlayersCount = PlayersCount } ) - - local PlayerTypesReport = REPORT:New() - - if PlayersCount > 0 then - for PlayerName, PlayerType in pairs( PlayerTypes ) do - PlayerTypesReport:Add( string.format('"%s" in %s', PlayerName, PlayerType ) ) - end - else - PlayerTypesReport:Add( "-" ) - end - - - return PlayersCount, PlayerTypesReport - end - - function TASK_A2A_DISPATCHER:RemoveTask( TaskIndex ) - self.Mission:RemoveTask( self.Tasks[TaskIndex] ) - self.Tasks[TaskIndex] = nil - end - - - --- Assigns tasks in relation to the detected items to the @{Set#SET_GROUP}. - -- @param #TASK_A2A_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Detection#DETECTION_BASE} derived object. - -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. - function TASK_A2A_DISPATCHER:ProcessDetected( Detection ) - self:E() - - local AreaMsg = {} - local TaskMsg = {} - local ChangeMsg = {} - - local Mission = self.Mission - - if Mission:IsIDLE() or Mission:IsENGAGED() then - - local TaskReport = REPORT:New() - - -- Checking the task queue for the dispatcher, and removing any obsolete task! - for TaskIndex, TaskData in pairs( self.Tasks ) do - local Task = TaskData -- Tasking.Task#TASK - if Task:IsStatePlanned() then - local DetectedItem = Detection:GetDetectedItem( TaskIndex ) - if not DetectedItem then - local TaskText = Task:GetName() - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - Mission:GetCommandCenter():MessageToGroup( string.format( "Obsolete A2A task %s for %s removed.", TaskText, Mission:GetName() ), TaskGroup ) - end - Task = self:RemoveTask( TaskIndex ) - end - end - end - - -- Now that all obsolete tasks are removed, loop through the detected targets. - for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do - - local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem - local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT - local DetectedCount = DetectedSet:Count() - local DetectedZone = DetectedItem.Zone - --self:E( { "Targets in DetectedItem", DetectedItem.ItemID, DetectedSet:Count(), tostring( DetectedItem ) } ) - --DetectedSet:Flush() - - local DetectedID = DetectedItem.ID - local TaskIndex = DetectedItem.Index - local DetectedItemChanged = DetectedItem.Changed - - local Task = self.Tasks[TaskIndex] - Task = self:EvaluateRemoveTask( Mission, Task, Detection, DetectedItem, TaskIndex, DetectedItemChanged ) -- Task will be removed if it is planned and changed. - - -- Evaluate INTERCEPT - if not Task and DetectedCount > 0 then - local TargetSetUnit = self:EvaluateENGAGE( DetectedItem ) -- Returns a SetUnit if there are targets to be INTERCEPTed... - if TargetSetUnit then - Task = TASK_A2A_ENGAGE:New( Mission, self.SetGroup, string.format( "ENGAGE.%03d", DetectedID ), TargetSetUnit ) - Task:SetDetection( Detection, TaskIndex ) - else - local TargetSetUnit = self:EvaluateINTERCEPT( DetectedItem ) -- Returns a SetUnit if there are targets to be INTERCEPTed... - if TargetSetUnit then - Task = TASK_A2A_INTERCEPT:New( Mission, self.SetGroup, string.format( "INTERCEPT.%03d", DetectedID ), TargetSetUnit ) - Task:SetDetection( Detection, TaskIndex ) - else - local TargetSetUnit = self:EvaluateSWEEP( DetectedItem ) -- Returns a SetUnit - if TargetSetUnit then - Task = TASK_A2A_SWEEP:New( Mission, self.SetGroup, string.format( "SWEEP.%03d", DetectedID ), TargetSetUnit ) - Task:SetDetection( Detection, TaskIndex ) - end - end - end - - if Task then - self.Tasks[TaskIndex] = Task - Task:SetTargetZone( DetectedZone, DetectedItem.Coordinate.y, DetectedItem.Coordinate.Heading ) - Task:SetDispatcher( self ) - Mission:AddTask( Task ) - - TaskReport:Add( Task:GetName() ) - else - self:E("This should not happen") - end - - end - - if Task then - local FriendliesCount, FriendliesReport = self:GetFriendliesNearBy( DetectedItem ) - Task:SetInfo( "Friendlies", string.format( "%d ( %s )", FriendliesCount, FriendliesReport:Text( "," ) ), 30 ) - local PlayersCount, PlayersReport = self:GetPlayerFriendliesNearBy( DetectedItem ) - Task:SetInfo( "Players", string.format( "%d ( %s )", PlayersCount, PlayersReport:Text( "," ) ), 31 ) - end - - -- OK, so the tasking has been done, now delete the changes reported for the area. - Detection:AcceptChanges( DetectedItem ) - end - - -- TODO set menus using the HQ coordinator - Mission:GetCommandCenter():SetMenu() - - local TaskText = TaskReport:Text(", ") - - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" then - Mission:GetCommandCenter():MessageToGroup( string.format( "%s has tasks %s. Subscribe to a task using the radio menu.", Mission:GetName(), TaskText ), TaskGroup ) - end - end - - end - - return true - end - -end ---- **Tasking** - The TASK_A2A models tasks for players in Air to Air engagements. --- --- ![Banner Image](..\Presentations\TASK_A2A\Dia1.JPG) --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- ==== --- --- @module Task_A2A - -do -- TASK_A2A - - --- The TASK_A2A class - -- @type TASK_A2A - -- @field Set#SET_UNIT TargetSetUnit - -- @extends Tasking.Task#TASK - - --- # TASK_A2A class, extends @{Task#TASK} - -- - -- The TASK_A2A class defines Air To Air tasks for a @{Set} of Target Units, - -- based on the tasking capabilities defined in @{Task#TASK}. - -- The TASK_A2A is implemented using a @{Fsm#FSM_TASK}, and has the following statuses: - -- - -- * **None**: Start of the process - -- * **Planned**: The A2A task is planned. - -- * **Assigned**: The A2A task is assigned to a @{Group#GROUP}. - -- * **Success**: The A2A task is successfully completed. - -- * **Failed**: The A2A task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. - -- - -- # 1.1) Set the scoring of achievements in an A2A attack. - -- - -- Scoring or penalties can be given in the following circumstances: - -- - -- * @{#TASK_A2A.SetScoreOnDestroy}(): Set a score when a target in scope of the A2A attack, has been destroyed. - -- * @{#TASK_A2A.SetScoreOnSuccess}(): Set a score when all the targets in scope of the A2A attack, have been destroyed. - -- * @{#TASK_A2A.SetPenaltyOnFailed}(): Set a penalty when the A2A attack has failed. - -- - -- @field #TASK_A2A - TASK_A2A = { - ClassName = "TASK_A2A", - } - - --- Instantiates a new TASK_A2A. - -- @param #TASK_A2A self - -- @param Tasking.Mission#MISSION Mission - -- @param Set#SET_GROUP SetAttack The set of groups for which the Task can be assigned. - -- @param #string TaskName The name of the Task. - -- @param Set#SET_UNIT UnitSetTargets - -- @param #number TargetDistance The distance to Target when the Player is considered to have "arrived" at the engagement range. - -- @param Core.Zone#ZONE_BASE TargetZone The target zone, if known. - -- If the TargetZone parameter is specified, the player will be routed to the center of the zone where all the targets are assumed to be. - -- @return #TASK_A2A self - function TASK_A2A:New( Mission, SetAttack, TaskName, TargetSetUnit, TaskType, TaskBriefing ) - local self = BASE:Inherit( self, TASK:New( Mission, SetAttack, TaskName, TaskType, TaskBriefing ) ) -- Tasking.Task#TASK_A2A - self:F() - - self.TargetSetUnit = TargetSetUnit - self.TaskType = TaskType - - local Fsm = self:GetUnitProcess() - - - Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "RouteToRendezVous", Rejected = "Reject" } ) - - Fsm:AddTransition( "Assigned", "RouteToRendezVous", "RoutingToRendezVous" ) - Fsm:AddProcess ( "RoutingToRendezVous", "RouteToRendezVousPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtRendezVous" } ) - Fsm:AddProcess ( "RoutingToRendezVous", "RouteToRendezVousZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtRendezVous" } ) - - Fsm:AddTransition( { "Arrived", "RoutingToRendezVous" }, "ArriveAtRendezVous", "ArrivedAtRendezVous" ) - - Fsm:AddTransition( { "ArrivedAtRendezVous", "HoldingAtRendezVous" }, "Engage", "Engaging" ) - Fsm:AddTransition( { "ArrivedAtRendezVous", "HoldingAtRendezVous" }, "HoldAtRendezVous", "HoldingAtRendezVous" ) - - Fsm:AddProcess ( "Engaging", "Account", ACT_ACCOUNT_DEADS:New(), {} ) - Fsm:AddTransition( "Engaging", "RouteToTarget", "Engaging" ) - Fsm:AddProcess( "Engaging", "RouteToTargetZone", ACT_ROUTE_ZONE:New(), {} ) - Fsm:AddProcess( "Engaging", "RouteToTargetPoint", ACT_ROUTE_POINT:New(), {} ) - Fsm:AddTransition( "Engaging", "RouteToTargets", "Engaging" ) - --- Fsm:AddTransition( "Accounted", "DestroyedAll", "Accounted" ) --- Fsm:AddTransition( "Accounted", "Success", "Success" ) - Fsm:AddTransition( "Rejected", "Reject", "Aborted" ) - Fsm:AddTransition( "Failed", "Fail", "Failed" ) - - - --- Test - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_A2A#TASK_A2A Task - function Fsm:onafterRouteToRendezVous( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - -- Determine the first Unit from the self.RendezVousSetUnit - - if Task:GetRendezVousZone( TaskUnit ) then - self:__RouteToRendezVousZone( 0.1 ) - else - if Task:GetRendezVousCoordinate( TaskUnit ) then - self:__RouteToRendezVousPoint( 0.1 ) - else - self:__ArriveAtRendezVous( 0.1 ) - end - end - end - - --- Test - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task#TASK_A2A Task - function Fsm:OnAfterArriveAtRendezVous( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - -- Determine the first Unit from the self.TargetSetUnit - - self:__Engage( 0.1 ) - end - - --- Test - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task#TASK_A2A Task - function Fsm:onafterEngage( TaskUnit, Task ) - self:E( { self } ) - self:__Account( 0.1 ) - self:__RouteToTarget(0.1 ) - self:__RouteToTargets( -10 ) - end - - --- Test - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_A2A#TASK_A2A Task - function Fsm:onafterRouteToTarget( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - -- Determine the first Unit from the self.TargetSetUnit - - if Task:GetTargetZone( TaskUnit ) then - self:__RouteToTargetZone( 0.1 ) - else - local TargetUnit = Task.TargetSetUnit:GetFirst() -- Wrapper.Unit#UNIT - if TargetUnit then - local Coordinate = TargetUnit:GetCoordinate() - self:T( { TargetCoordinate = Coordinate, Coordinate:GetX(), Coordinate:GetAlt(), Coordinate:GetZ() } ) - Task:SetTargetCoordinate( TargetUnit:GetCoordinate(), TaskUnit ) - end - self:__RouteToTargetPoint( 0.1 ) - end - end - - --- Test - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_A2A#TASK_A2A Task - function Fsm:onafterRouteToTargets( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - local TargetUnit = Task.TargetSetUnit:GetFirst() -- Wrapper.Unit#UNIT - if TargetUnit then - Task:SetTargetCoordinate( TargetUnit:GetCoordinate(), TaskUnit ) - end - self:__RouteToTargets( -10 ) - end - - return self - - end - - --- @param #TASK_A2A self - function TASK_A2A:GetPlannedMenuText() - return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" - end - - --- @param #TASK_A2A self - -- @param Core.Point#COORDINATE RendezVousCoordinate The Coordinate object referencing to the 2D point where the RendezVous point is located on the map. - -- @param #number RendezVousRange The RendezVousRange that defines when the player is considered to have arrived at the RendezVous point. - -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_A2A:SetRendezVousCoordinate( RendezVousCoordinate, RendezVousRange, TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT - ActRouteRendezVous:SetCoordinate( RendezVousCoordinate ) - ActRouteRendezVous:SetRange( RendezVousRange ) - end - - --- @param #TASK_A2A self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return Core.Point#COORDINATE The Coordinate object referencing to the 2D point where the RendezVous point is located on the map. - -- @return #number The RendezVousRange that defines when the player is considered to have arrived at the RendezVous point. - function TASK_A2A:GetRendezVousCoordinate( TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT - return ActRouteRendezVous:GetCoordinate(), ActRouteRendezVous:GetRange() - end - - - - --- @param #TASK_A2A self - -- @param Core.Zone#ZONE_BASE RendezVousZone The Zone object where the RendezVous is located on the map. - -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_A2A:SetRendezVousZone( RendezVousZone, TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE - ActRouteRendezVous:SetZone( RendezVousZone ) - end - - --- @param #TASK_A2A self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return Core.Zone#ZONE_BASE The Zone object where the RendezVous is located on the map. - function TASK_A2A:GetRendezVousZone( TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE - return ActRouteRendezVous:GetZone() - end - - --- @param #TASK_A2A self - -- @param Core.Point#COORDINATE TargetCoordinate The Coordinate object where the Target is located on the map. - -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_A2A:SetTargetCoordinate( TargetCoordinate, TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT - ActRouteTarget:SetCoordinate( TargetCoordinate ) - end - - - --- @param #TASK_A2A self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return Core.Point#COORDINATE The Coordinate object where the Target is located on the map. - function TASK_A2A:GetTargetCoordinate( TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT - return ActRouteTarget:GetCoordinate() - end - - - --- @param #TASK_A2A self - -- @param Core.Zone#ZONE_BASE TargetZone The Zone object where the Target is located on the map. - -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_A2A:SetTargetZone( TargetZone, Altitude, Heading, TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE - ActRouteTarget:SetZone( TargetZone, Altitude, Heading ) - end - - - --- @param #TASK_A2A self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return Core.Zone#ZONE_BASE The Zone object where the Target is located on the map. - function TASK_A2A:GetTargetZone( TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE - return ActRouteTarget:GetZone() - end - - function TASK_A2A:SetGoalTotal() - - self.GoalTotal = self.TargetSetUnit:Count() - end - - function TASK_A2A:GetGoalTotal() - - return self.GoalTotal - end - - - -end - - -do -- TASK_A2A_INTERCEPT - - --- The TASK_A2A_INTERCEPT class - -- @type TASK_A2A_INTERCEPT - -- @field Set#SET_UNIT TargetSetUnit - -- @extends Tasking.Task#TASK - - --- # TASK_A2A_INTERCEPT class, extends @{Task_A2A#TASK_A2A} - -- - -- The TASK_A2A_INTERCEPT class defines an intercept task for a human player to be executed. - -- When enemy planes need to be intercepted by human players, use this task type to urgen the players to get out there! - -- - -- The TASK_A2A_INTERCEPT is used by the @{Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create intercept tasks - -- based on detected airborne enemy targets intruding friendly airspace. - -- - -- The task is defined for a @{Mission#MISSION}, where a friendly @{Set#SET_GROUP} consisting of GROUPs with one human players each, is intercepting the targets. - -- The task is given a name and a briefing, that is used in the menu structure and in the reporting. - -- - -- @field #TASK_A2A_INTERCEPT - TASK_A2A_INTERCEPT = { - ClassName = "TASK_A2A_INTERCEPT", - } - - - - --- Instantiates a new TASK_A2A_INTERCEPT. - -- @param #TASK_A2A_INTERCEPT self - -- @param Tasking.Mission#MISSION Mission - -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. - -- @param #string TaskName The name of the Task. - -- @param Core.Set#SET_UNIT TargetSetUnit - -- @param #string TaskBriefing The briefing of the task. - -- @return #TASK_A2A_INTERCEPT - function TASK_A2A_INTERCEPT:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing ) - local self = BASE:Inherit( self, TASK_A2A:New( Mission, SetGroup, TaskName, TargetSetUnit, "INTERCEPT", TaskBriefing ) ) -- #TASK_A2A_INTERCEPT - self:F() - - Mission:AddTask( self ) - - --TODO: Add BR, Altitude, type of planes... - - self:SetBriefing( - TaskBriefing or - "Intercept incoming intruders.\n" - ) - - self:UpdateTaskInfo() - - return self - end - - function TASK_A2A_INTERCEPT:UpdateTaskInfo() - - local TargetCoordinate = self.Detection and self.Detection:GetDetectedItemCoordinate( self.DetectedItemIndex ) or self.TargetSetUnit:GetFirst():GetCoordinate() - self:SetInfo( "Coordinates", TargetCoordinate, 0 ) - - self:SetInfo( "Threat", "[" .. string.rep( "■", self.Detection and self.Detection:GetDetectedItemThreatLevel( self.DetectedItemIndex ) or self.TargetSetUnit:CalculateThreatLevelA2G() ) .. "]", 11 ) - - if self.Detection then - local DetectedItemsCount = self.TargetSetUnit:Count() - local ReportTypes = REPORT:New() - local TargetTypes = {} - for TargetUnitName, TargetUnit in pairs( self.TargetSetUnit:GetSet() ) do - local TargetType = self.Detection:GetDetectedUnitTypeName( TargetUnit ) - if not TargetTypes[TargetType] then - TargetTypes[TargetType] = TargetType - ReportTypes:Add( TargetType ) - end - end - self:SetInfo( "Targets", string.format( "%d of %s", DetectedItemsCount, ReportTypes:Text( ", " ) ), 10 ) - else - local DetectedItemsCount = self.TargetSetUnit:Count() - local DetectedItemsTypes = self.TargetSetUnit:GetTypeNames() - self:SetInfo( "Targets", string.format( "%d of %s", DetectedItemsCount, DetectedItemsTypes ), 10 ) - end - - end - - - --- @param #TASK_A2A_INTERCEPT self - -- @param Wrapper.Group#GROUP ReportGroup - function TASK_A2A_INTERCEPT:ReportOrder( ReportGroup ) - self:F( { TaskInfo = self.TaskInfo } ) - local Coordinate = self.TaskInfo.Coordinates.TaskInfoText - local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate ) - - return Distance - end - - - --- @param #TASK_A2A_INTERCEPT self - function TASK_A2A_INTERCEPT:onafterGoal( TaskUnit, From, Event, To ) - local TargetSetUnit = self.TargetSetUnit -- Core.Set#SET_UNIT - - if TargetSetUnit:Count() == 0 then - self:Success() - end - - self:__Goal( -10 ) - end - - --- Set a score when a target in scope of the A2A attack, has been destroyed . - -- @param #TASK_A2A_INTERCEPT self - -- @param #string PlayerName The name of the player. - -- @param #number Score The score in points to be granted when task process has been achieved. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2A_INTERCEPT - function TASK_A2A_INTERCEPT:SetScoreOnProgress( PlayerName, Score, TaskUnit ) - self:F( { PlayerName, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has intercepted a target.", Score ) - - return self - end - - --- Set a score when all the targets in scope of the A2A attack, have been destroyed. - -- @param #TASK_A2A_INTERCEPT self - -- @param #string PlayerName The name of the player. - -- @param #number Score The score in points. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2A_INTERCEPT - function TASK_A2A_INTERCEPT:SetScoreOnSuccess( PlayerName, Score, TaskUnit ) - self:F( { PlayerName, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScore( "Success", "All targets have been successfully intercepted!", Score ) - - return self - end - - --- Set a penalty when the A2A attack has failed. - -- @param #TASK_A2A_INTERCEPT self - -- @param #string PlayerName The name of the player. - -- @param #number Penalty The penalty in points, must be a negative value! - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2A_INTERCEPT - function TASK_A2A_INTERCEPT:SetScoreOnFail( PlayerName, Penalty, TaskUnit ) - self:F( { PlayerName, Penalty, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScore( "Failed", "The intercept has failed!", Penalty ) - - return self - end - - -end - - -do -- TASK_A2A_SWEEP - - --- The TASK_A2A_SWEEP class - -- @type TASK_A2A_SWEEP - -- @field Set#SET_UNIT TargetSetUnit - -- @extends Tasking.Task#TASK - - --- # TASK_A2A_SWEEP class, extends @{Task_A2A#TASK_A2A} - -- - -- The TASK_A2A_SWEEP class defines a sweep task for a human player to be executed. - -- A sweep task needs to be given when targets were detected but somehow the detection was lost. - -- Most likely, these enemy planes are hidden in the mountains or are flying under radar. - -- These enemy planes need to be sweeped by human players, and use this task type to urge the players to get out there and find those enemy fighters. - -- - -- The TASK_A2A_SWEEP is used by the @{Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create sweep tasks - -- based on detected airborne enemy targets intruding friendly airspace, for which the detection has been lost for more than 60 seconds. - -- - -- The task is defined for a @{Mission#MISSION}, where a friendly @{Set#SET_GROUP} consisting of GROUPs with one human players each, is sweeping the targets. - -- The task is given a name and a briefing, that is used in the menu structure and in the reporting. - -- - -- @field #TASK_A2A_SWEEP - TASK_A2A_SWEEP = { - ClassName = "TASK_A2A_SWEEP", - } - - - - --- Instantiates a new TASK_A2A_SWEEP. - -- @param #TASK_A2A_SWEEP self - -- @param Tasking.Mission#MISSION Mission - -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. - -- @param #string TaskName The name of the Task. - -- @param Core.Set#SET_UNIT TargetSetUnit - -- @param #string TaskBriefing The briefing of the task. - -- @return #TASK_A2A_SWEEP self - function TASK_A2A_SWEEP:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing ) - local self = BASE:Inherit( self, TASK_A2A:New( Mission, SetGroup, TaskName, TargetSetUnit, "SWEEP", TaskBriefing ) ) -- #TASK_A2A_SWEEP - self:F() - - Mission:AddTask( self ) - - --TODO: Add BR, Altitude, type of planes... - - self:SetBriefing( - TaskBriefing or - "Perform a fighter sweep. Incoming intruders were detected and could be hiding at the location.\n" - ) - - self:UpdateTaskInfo() - - return self - end - - - function TASK_A2A_SWEEP:UpdateTaskInfo() - - local TargetCoordinate = self.Detection and self.Detection:GetDetectedItemCoordinate( self.DetectedItemIndex ) or self.TargetSetUnit:GetFirst():GetCoordinate() - self:SetInfo( "Coordinates", TargetCoordinate, 0 ) - - self:SetInfo( "Assumed Threat", "[" .. string.rep( "■", self.Detection and self.Detection:GetDetectedItemThreatLevel( self.DetectedItemIndex ) or self.TargetSetUnit:CalculateThreatLevelA2G() ) .. "]", 11 ) - - if self.Detection then - local DetectedItemsCount = self.TargetSetUnit:Count() - local ReportTypes = REPORT:New() - local TargetTypes = {} - for TargetUnitName, TargetUnit in pairs( self.TargetSetUnit:GetSet() ) do - local TargetType = self.Detection:GetDetectedUnitTypeName( TargetUnit ) - if not TargetTypes[TargetType] then - TargetTypes[TargetType] = TargetType - ReportTypes:Add( TargetType ) - end - end - self:SetInfo( "Lost Targets", string.format( "%d of %s", DetectedItemsCount, ReportTypes:Text( ", " ) ), 10 ) - else - local DetectedItemsCount = self.TargetSetUnit:Count() - local DetectedItemsTypes = self.TargetSetUnit:GetTypeNames() - self:SetInfo( "Lost Targets", string.format( "%d of %s", DetectedItemsCount, DetectedItemsTypes ), 10 ) - end - - end - - - function TASK_A2A_SWEEP:ReportOrder( ReportGroup ) - local Coordinate = self.TaskInfo.Coordinates.TaskInfoText - local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate ) - - return Distance - end - - --- @param #TASK_A2A_SWEEP self - function TASK_A2A_SWEEP:onafterGoal( TaskUnit, From, Event, To ) - local TargetSetUnit = self.TargetSetUnit -- Core.Set#SET_UNIT - - if TargetSetUnit:Count() == 0 then - self:Success() - end - - self:__Goal( -10 ) - end - - --- Set a score when a target in scope of the A2A attack, has been destroyed . - -- @param #TASK_A2A_SWEEP self - -- @param #string PlayerName The name of the player. - -- @param #number Score The score in points to be granted when task process has been achieved. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2A_SWEEP - function TASK_A2A_SWEEP:SetScoreOnProgress( PlayerName, Score, TaskUnit ) - self:F( { PlayerName, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has sweeped a target.", Score ) - - return self - end - - --- Set a score when all the targets in scope of the A2A attack, have been destroyed. - -- @param #TASK_A2A_SWEEP self - -- @param #string PlayerName The name of the player. - -- @param #number Score The score in points. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2A_SWEEP - function TASK_A2A_SWEEP:SetScoreOnSuccess( PlayerName, Score, TaskUnit ) - self:F( { PlayerName, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScore( "Success", "All targets have been successfully sweeped!", Score ) - - return self - end - - --- Set a penalty when the A2A attack has failed. - -- @param #TASK_A2A_SWEEP self - -- @param #string PlayerName The name of the player. - -- @param #number Penalty The penalty in points, must be a negative value! - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2A_SWEEP - function TASK_A2A_SWEEP:SetScoreOnFail( PlayerName, Penalty, TaskUnit ) - self:F( { PlayerName, Penalty, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScore( "Failed", "The sweep has failed!", Penalty ) - - return self - end - -end - - -do -- TASK_A2A_ENGAGE - - --- The TASK_A2A_ENGAGE class - -- @type TASK_A2A_ENGAGE - -- @field Set#SET_UNIT TargetSetUnit - -- @extends Tasking.Task#TASK - - --- # TASK_A2A_ENGAGE class, extends @{Task_A2A#TASK_A2A} - -- - -- The TASK_A2A_ENGAGE class defines an engage task for a human player to be executed. - -- When enemy planes are close to human players, use this task type is used urge the players to get out there! - -- - -- The TASK_A2A_ENGAGE is used by the @{Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create engage tasks - -- based on detected airborne enemy targets intruding friendly airspace. - -- - -- The task is defined for a @{Mission#MISSION}, where a friendly @{Set#SET_GROUP} consisting of GROUPs with one human players each, is engaging the targets. - -- The task is given a name and a briefing, that is used in the menu structure and in the reporting. - -- - -- @field #TASK_A2A_ENGAGE - TASK_A2A_ENGAGE = { - ClassName = "TASK_A2A_ENGAGE", - } - - - - --- Instantiates a new TASK_A2A_ENGAGE. - -- @param #TASK_A2A_ENGAGE self - -- @param Tasking.Mission#MISSION Mission - -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. - -- @param #string TaskName The name of the Task. - -- @param Core.Set#SET_UNIT TargetSetUnit - -- @param #string TaskBriefing The briefing of the task. - -- @return #TASK_A2A_ENGAGE self - function TASK_A2A_ENGAGE:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing ) - local self = BASE:Inherit( self, TASK_A2A:New( Mission, SetGroup, TaskName, TargetSetUnit, "ENGAGE", TaskBriefing ) ) -- #TASK_A2A_ENGAGE - self:F() - - Mission:AddTask( self ) - - --TODO: Add BR, Altitude, type of planes... - - self:SetBriefing( - TaskBriefing or - "Bogeys are nearby! Players close by are ordered to ENGAGE the intruders!\n" - ) - - self:UpdateTaskInfo() - - return self - end - - - function TASK_A2A_ENGAGE:UpdateTaskInfo() - - local TargetCoordinate = self.Detection and self.Detection:GetDetectedItemCoordinate( self.DetectedItemIndex ) or self.TargetSetUnit:GetFirst():GetCoordinate() - self:SetInfo( "Coordinates", TargetCoordinate, 0 ) - - self:SetInfo( "Threat", "[" .. string.rep( "■", self.Detection and self.Detection:GetDetectedItemThreatLevel( self.DetectedItemIndex ) or self.TargetSetUnit:CalculateThreatLevelA2G() ) .. "]", 11 ) - - if self.Detection then - local DetectedItemsCount = self.TargetSetUnit:Count() - local ReportTypes = REPORT:New() - local TargetTypes = {} - for TargetUnitName, TargetUnit in pairs( self.TargetSetUnit:GetSet() ) do - local TargetType = self.Detection:GetDetectedUnitTypeName( TargetUnit ) - if not TargetTypes[TargetType] then - TargetTypes[TargetType] = TargetType - ReportTypes:Add( TargetType ) - end - end - self:SetInfo( "Targets", string.format( "%d of %s", DetectedItemsCount, ReportTypes:Text( ", " ) ), 10 ) - else - local DetectedItemsCount = self.TargetSetUnit:Count() - local DetectedItemsTypes = self.TargetSetUnit:GetTypeNames() - self:SetInfo( "Targets", string.format( "%d of %s", DetectedItemsCount, DetectedItemsTypes ), 10 ) - end - - end - - function TASK_A2A_ENGAGE:ReportOrder( ReportGroup ) - local Coordinate = self.TaskInfo.Coordinates.TaskInfoText - local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate ) - - return Distance - end - - --- @param #TASK_A2A_ENGAGE self - function TASK_A2A_ENGAGE:onafterGoal( TaskUnit, From, Event, To ) - local TargetSetUnit = self.TargetSetUnit -- Core.Set#SET_UNIT - - if TargetSetUnit:Count() == 0 then - self:Success() - end - - self:__Goal( -10 ) - end - - --- Set a score when a target in scope of the A2A attack, has been destroyed . - -- @param #TASK_A2A_ENGAGE self - -- @param #string PlayerName The name of the player. - -- @param #number Score The score in points to be granted when task process has been achieved. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2A_ENGAGE - function TASK_A2A_ENGAGE:SetScoreOnProgress( PlayerName, Score, TaskUnit ) - self:F( { PlayerName, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has engaged and destroyed a target.", Score ) - - return self - end - - --- Set a score when all the targets in scope of the A2A attack, have been destroyed. - -- @param #TASK_A2A_ENGAGE self - -- @param #string PlayerName The name of the player. - -- @param #number Score The score in points. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2A_ENGAGE - function TASK_A2A_ENGAGE:SetScoreOnSuccess( PlayerName, Score, TaskUnit ) - self:F( { PlayerName, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScore( "Success", "All targets have been successfully engaged!", Score ) - - return self - end - - --- Set a penalty when the A2A attack has failed. - -- @param #TASK_A2A_ENGAGE self - -- @param #string PlayerName The name of the player. - -- @param #number Penalty The penalty in points, must be a negative value! - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2A_ENGAGE - function TASK_A2A_ENGAGE:SetScoreOnFail( PlayerName, Penalty, TaskUnit ) - self:F( { PlayerName, Penalty, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScore( "Failed", "The target engagement has failed!", Penalty ) - - return self - end - -end - ---- **Tasking** -- The TASK_CARGO models tasks for players to transport @{Cargo}. --- --- ![Banner Image](..\Presentations\TASK_CARGO\Dia1.JPG) --- --- ==== --- --- The Moose framework provides various CARGO classes that allow DCS phisical or logical objects to be transported or sling loaded by Carriers. --- The CARGO_ classes, as part of the moose core, are able to Board, Load, UnBoard and UnLoad cargo between Carrier units. --- --- This collection of classes in this module define tasks for human players to handle these cargo objects. --- Cargo can be transported, picked-up, deployed and sling-loaded from and to other places. --- --- The following classes are important to consider: --- --- * @{#TASK_CARGO_TRANSPORT}: Defines a task for a human player to transport a set of cargo between various zones. --- --- ==== --- --- ### Author: **Sven Van de Velde (FlightControl)** --- --- ### Contributions: --- --- ==== --- --- @module Task_Cargo - -do -- TASK_CARGO - - --- @type TASK_CARGO - -- @extends Tasking.Task#TASK - - --- - -- # TASK_CARGO class, extends @{Task#TASK} - -- - -- ## A flexible tasking system - -- - -- The TASK_CARGO classes provide you with a flexible tasking sytem, - -- that allows you to transport cargo of various types between various locations - -- and various dedicated deployment zones. - -- - -- The cargo in scope of the TASK_CARGO classes must be explicitly given, and is of type SET_CARGO. - -- The SET_CARGO contains a collection of CARGO objects that must be handled by the players in the mission. - -- - -- - -- ## Task execution experience from the player perspective - -- - -- A human player can join the battle field in a client airborne slot or a ground vehicle within the CA module (ALT-J). - -- The player needs to accept the task from the task overview list within the mission, using the radio menus. - -- - -- Once the TASK_CARGO is assigned to the player and accepted by the player, the player will obtain - -- an extra **Cargo Handling Radio Menu** that contains the CARGO objects that need to be transported. - -- - -- Each CARGO object has a certain state: - -- - -- * **UnLoaded**: The CARGO is located within the battlefield. It may still need to be transported. - -- * **Loaded**: The CARGO is loaded within a Carrier. This can be your air unit, or another air unit, or even a vehicle. - -- * **Boarding**: The CARGO is running or moving towards your Carrier for loading. - -- * **UnBoarding**: The CARGO is driving or jumping out of your Carrier and moves to a location in the Deployment Zone. - -- - -- Cargo must be transported towards different **Deployment @{Zone}s**. - -- - -- The Cargo Handling Radio Menu system allows to execute **various actions** to handle the cargo. - -- In the menu, you'll find for each CARGO, that is part of the scope of the task, various actions that can be completed. - -- Depending on the location of your Carrier unit, the menu options will vary. - -- - -- - -- ## Cargo Pickup and Boarding - -- - -- For cargo boarding, a cargo can only execute the boarding actions if it is within the foreseen **Reporting Range**. - -- Therefore, it is important that you steer your Carrier within the Reporting Range, - -- so that boarding actions can be executed on the cargo. - -- To Pickup and Board cargo, the following menu items will be shown in your carrier radio menu: - -- - -- ### Board Cargo - -- - -- If your Carrier is within the Reporting Range of the cargo, it will allow to pickup the cargo by selecting this menu option. - -- Depending on the Cargo type, the cargo will either move to your Carrier or you will receive instructions how to handle the cargo - -- pickup. If the cargo moves to your carrier, it will indicate the boarding status. - -- Note that multiple units need to board your Carrier, so it is required to await the full boarding process. - -- Once the cargo is fully boarded within your Carrier, you will be notified of this. - -- - -- Note that for airborne Carriers, it is required to land first before the Boarding process can be initiated. - -- If during boarding the Carrier gets airborne, the boarding process will be cancelled. - -- - -- ## Pickup Cargo - -- - -- If your Carrier is not within the Reporting Range of the cargo, the HQ will guide you to its location. - -- Routing information is shown in flight that directs you to the cargo within Reporting Range. - -- Upon arrival, the Cargo will contact you and further instructions will be given. - -- When your Carrier is airborne, you will receive instructions to land your Carrier. - -- The action will not be completed until you've landed your Carrier. - -- - -- - -- ## Cargo Deploy and UnBoarding - -- - -- Various Deployment Zones can be foreseen in the scope of the Cargo transportation. Each deployment zone can be of varying @{Zone} type. - -- The Cargo Handling Radio Menu provides with menu options to execute an action to steer your Carrier to a specific Zone. - -- - -- ### UnBoard Cargo - -- - -- If your Carrier is already within a Deployment Zone, - -- then the Cargo Handling Radio Menu allows to **UnBoard** a specific cargo that is - -- loaded within your Carrier group into the Deployment Zone. - -- Note that the Unboarding process takes a while, as the cargo units (infantry or vehicles) must unload from your Carrier. - -- Ensure that you stay at the position or stay on the ground while Unboarding. - -- If any unforeseen manoeuvre is done by the Carrier, then the Unboarding will be cancelled. - -- - -- ### Deploy Cargo - -- - -- If your Carrier is not within a Deployment Zone, you'll need to fly towards one. - -- Fortunately, the Cargo Handling Radio Menu provides you with menu options to select a specific Deployment Zone to fly towards. - -- Once a Deployment Zone has been selected, your Carrier will receive routing information from HQ towards the Deployment Zone center. - -- Upon arrival, the HQ will provide you with further instructions. - -- When your Carrier is airborne, you will receive instructions to land your Carrier. - -- The action will not be completed until you've landed your Carrier! - -- - -- ## Handle TASK_CARGO Events ... - -- - -- The TASK_CARGO classes define @{Cargo} transport tasks, - -- based on the tasking capabilities defined in @{Task#TASK}. - -- - -- ### Specific TASK_CARGO Events - -- - -- Specific Cargo Handling event can be captured, that allow to trigger specific actions! - -- - -- * **Boarded**: Triggered when the Cargo has been Boarded into your Carrier. - -- * **UnBoarded**: Triggered when the cargo has been Unboarded from your Carrier and has arrived at the Deployment Zone. - -- - -- ### Standard TASK_CARGO Events - -- - -- The TASK_CARGO is implemented using a @{Statemachine#FSM_TASK}, and has the following standard statuses: - -- - -- * **None**: Start of the process. - -- * **Planned**: The cargo task is planned. - -- * **Assigned**: The cargo task is assigned to a @{Group#GROUP}. - -- * **Success**: The cargo task is successfully completed. - -- * **Failed**: The cargo task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. - -- - -- === - -- - -- @field #TASK_CARGO - -- - TASK_CARGO = { - ClassName = "TASK_CARGO", - } - - --- Instantiates a new TASK_CARGO. - -- @param #TASK_CARGO self - -- @param Tasking.Mission#MISSION Mission - -- @param Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. - -- @param #string TaskName The name of the Task. - -- @param Core.Set#SET_CARGO SetCargo The scope of the cargo to be transported. - -- @param #string TaskType The type of Cargo task. - -- @param #string TaskBriefing The Cargo Task briefing. - -- @return #TASK_CARGO self - function TASK_CARGO:New( Mission, SetGroup, TaskName, SetCargo, TaskType, TaskBriefing ) - local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType, TaskBriefing ) ) -- #TASK_CARGO - self:F( {Mission, SetGroup, TaskName, SetCargo, TaskType}) - - self.SetCargo = SetCargo - self.TaskType = TaskType - self.SmokeColor = SMOKECOLOR.Red - - self.CargoItemCount = {} -- Map of Carriers having a cargo item count to check the cargo loading limits. - self.CargoLimit = 2 - - self.DeployZones = {} -- setmetatable( {}, { __mode = "v" } ) -- weak table on value - - - local Fsm = self:GetUnitProcess() - - Fsm:SetStartState( "Planned" ) - - Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "SelectAction", Rejected = "Reject" } ) - - Fsm:AddTransition( { "Assigned", "WaitingForCommand", "ArrivedAtPickup", "ArrivedAtDeploy", "Boarded", "UnBoarded", "Landed", "Boarding" }, "SelectAction", "*" ) - - Fsm:AddTransition( "*", "RouteToPickup", "RoutingToPickup" ) - Fsm:AddProcess ( "RoutingToPickup", "RouteToPickupPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtPickup", Cancelled = "CancelRouteToPickup" } ) - Fsm:AddTransition( "Arrived", "ArriveAtPickup", "ArrivedAtPickup" ) - Fsm:AddTransition( "Cancelled", "CancelRouteToPickup", "WaitingForCommand" ) - - Fsm:AddTransition( "*", "RouteToDeploy", "RoutingToDeploy" ) - Fsm:AddProcess ( "RoutingToDeploy", "RouteToDeployZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtDeploy", Cancelled = "CancelRouteToDeploy" } ) - Fsm:AddTransition( "Arrived", "ArriveAtDeploy", "ArrivedAtDeploy" ) - Fsm:AddTransition( "Cancelled", "CancelRouteToDeploy", "WaitingForCommand" ) - - Fsm:AddTransition( { "ArrivedAtPickup", "ArrivedAtDeploy", "Landing" }, "Land", "Landing" ) - Fsm:AddTransition( "Landing", "Landed", "Landed" ) - - Fsm:AddTransition( "*", "PrepareBoarding", "AwaitBoarding" ) - Fsm:AddTransition( "AwaitBoarding", "Board", "Boarding" ) - Fsm:AddTransition( "Boarding", "Boarded", "Boarded" ) - - Fsm:AddTransition( "*", "PrepareUnBoarding", "AwaitUnBoarding" ) - Fsm:AddTransition( "AwaitUnBoarding", "UnBoard", "UnBoarding" ) - Fsm:AddTransition( "UnBoarding", "UnBoarded", "UnBoarded" ) - - - Fsm:AddTransition( "Deployed", "Success", "Success" ) - Fsm:AddTransition( "Rejected", "Reject", "Aborted" ) - Fsm:AddTransition( "Failed", "Fail", "Failed" ) - - - --- - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_CARGO#TASK_CARGO Task - function Fsm:onafterSelectAction( TaskUnit, Task ) - - local TaskUnitName = TaskUnit:GetName() - - self:E( { TaskUnit = TaskUnitName, Task = Task and Task:GetClassNameAndID() } ) - - local MenuTime = timer.getTime() - - TaskUnit.Menu = MENU_GROUP:New( TaskUnit:GetGroup(), Task:GetName() .. " @ " .. TaskUnit:GetName() ):SetTime( MenuTime ) - - local CargoItemCount = TaskUnit:CargoItemCount() - - --Task:GetMission():GetCommandCenter():MessageToGroup( "Cargo in carrier: " .. CargoItemCount, TaskUnit:GetGroup() ) - - - Task.SetCargo:ForEachCargo( - - --- @param Core.Cargo#CARGO Cargo - function( Cargo ) - - if Cargo:IsAlive() then - --- if Task:is( "RoutingToPickup" ) then --- MENU_GROUP_COMMAND:New( --- TaskUnit:GetGroup(), --- "Cancel Route " .. Cargo.Name, --- TaskUnit.Menu, --- self.MenuRouteToPickupCancel, --- self, --- Cargo --- ):SetTime(MenuTime) --- end - - - - if Cargo:IsUnLoaded() then - if CargoItemCount < Task.CargoLimit then - if Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then - local NotInDeployZones = true - for DeployZoneName, DeployZone in pairs( Task.DeployZones ) do - if Cargo:IsInZone( DeployZone ) then - NotInDeployZones = false - end - end - if NotInDeployZones then - if not TaskUnit:InAir() then - MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), "Board cargo " .. Cargo.Name, TaskUnit.Menu, self.MenuBoardCargo, self, Cargo ):SetTime(MenuTime) - end - end - else - MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), "Route to Pickup cargo " .. Cargo.Name, TaskUnit.Menu, self.MenuRouteToPickup, self, Cargo ):SetTime(MenuTime) - end - end - end - - if Cargo:IsLoaded() then - if not TaskUnit:InAir() then - MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), "Unboard cargo " .. Cargo.Name, TaskUnit.Menu, self.MenuUnBoardCargo, self, Cargo ):SetTime(MenuTime) - end - -- Deployzones are optional zones that can be selected to request routing information. - for DeployZoneName, DeployZone in pairs( Task.DeployZones ) do - if not Cargo:IsInZone( DeployZone ) then - MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), "Route to Deploy cargo at " .. DeployZoneName, TaskUnit.Menu, self.MenuRouteToDeploy, self, DeployZone ):SetTime(MenuTime) - end - end - end - end - - end - ) - - TaskUnit.Menu:Remove( MenuTime ) - - - self:__SelectAction( -15 ) - - end - - - --- - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:OnLeaveWaitingForCommand( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - - TaskUnit.Menu:Remove() - end - - function Fsm:MenuBoardCargo( Cargo ) - self:__PrepareBoarding( 1.0, Cargo ) - end - - function Fsm:MenuUnBoardCargo( Cargo, DeployZone ) - self:__PrepareUnBoarding( 1.0, Cargo, DeployZone ) - end - - function Fsm:MenuRouteToPickup( Cargo ) - self:__RouteToPickup( 1.0, Cargo ) - end - - function Fsm:MenuRouteToDeploy( DeployZone ) - self:__RouteToDeploy( 1.0, DeployZone ) - end - - - - --- - --#TASK_CAROG_TRANSPORT self - --#Wrapper.Unit#UNIT - - - --- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_Cargo#TASK_CARGO Task - -- @param From - -- @param Event - -- @param To - -- @param Core.Cargo#CARGO Cargo - function Fsm:onafterRouteToPickup( TaskUnit, Task, From, Event, To, Cargo ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - - if Cargo:IsAlive() then - self.Cargo = Cargo -- Core.Cargo#CARGO - Task:SetCargoPickup( self.Cargo, TaskUnit ) - self:__RouteToPickupPoint( -0.1 ) - end - - end - - - - --- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:onafterArriveAtPickup( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - if self.Cargo:IsAlive() then - self.Cargo:Smoke( Task:GetSmokeColor(), 15 ) - if TaskUnit:IsAir() then - Task:GetMission():GetCommandCenter():MessageToGroup( "Land", TaskUnit:GetGroup() ) - self:__Land( -0.1, "Pickup" ) - else - self:__SelectAction( -0.1 ) - end - end - end - - - --- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:onafterCancelRouteToPickup( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - - self:__SelectAction( -0.1 ) - end - - - --- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - function Fsm:onafterRouteToDeploy( TaskUnit, Task, From, Event, To, DeployZone ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - - self:E( DeployZone ) - self.DeployZone = DeployZone - Task:SetDeployZone( self.DeployZone, TaskUnit ) - self:__RouteToDeployZone( -0.1 ) - end - - - --- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:onafterArriveAtDeploy( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - - if TaskUnit:IsAir() then - Task:GetMission():GetCommandCenter():MessageToGroup( "Land", TaskUnit:GetGroup() ) - self:__Land( -0.1, "Deploy" ) - else - self:__SelectAction( -0.1 ) - end - end - - - --- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:onafterCancelRouteToDeploy( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - - self:__SelectAction( -0.1 ) - end - - - - --- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:onafterLand( TaskUnit, Task, From, Event, To, Action ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - - if self.Cargo:IsAlive() then - if self.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then - if TaskUnit:InAir() then - self:__Land( -10, Action ) - else - Task:GetMission():GetCommandCenter():MessageToGroup( "Landed ...", TaskUnit:GetGroup() ) - self:__Landed( -0.1, Action ) - end - else - if Action == "Pickup" then - self:__RouteToPickupZone( -0.1 ) - else - self:__RouteToDeployZone( -0.1 ) - end - end - end - end - - --- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:onafterLanded( TaskUnit, Task, From, Event, To, Action ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - - if self.Cargo:IsAlive() then - if self.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then - if TaskUnit:InAir() then - self:__Land( -0.1, Action ) - else - self:__SelectAction( -0.1 ) - end - else - if Action == "Pickup" then - self:__RouteToPickupZone( -0.1 ) - else - self:__RouteToDeployZone( -0.1 ) - end - end - end - end - - --- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:onafterPrepareBoarding( TaskUnit, Task, From, Event, To, Cargo ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - - if Cargo and Cargo:IsAlive() then - self.Cargo = Cargo -- Core.Cargo#CARGO_GROUP - self:__Board( -0.1 ) - end - end - - --- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:onafterBoard( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - - function self.Cargo:OnEnterLoaded( From, Event, To, TaskUnit, TaskProcess ) - self:E({From, Event, To, TaskUnit, TaskProcess }) - TaskProcess:__Boarded( 0.1 ) - end - - if self.Cargo:IsAlive() then - if self.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then - if TaskUnit:InAir() then - --- ABORT the boarding. Split group if any and go back to select action. - else - self.Cargo:MessageToGroup( "Boarding ...", TaskUnit:GetGroup() ) - self.Cargo:Board( TaskUnit, 20, self ) - end - else - --self:__ArriveAtCargo( -0.1 ) - end - end - end - - - --- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:onafterBoarded( TaskUnit, Task ) - - local TaskUnitName = TaskUnit:GetName() - self:E( { TaskUnit = TaskUnitName, Task = Task and Task:GetClassNameAndID() } ) - - self.Cargo:MessageToGroup( "Boarded ...", TaskUnit:GetGroup() ) - - TaskUnit:AddCargo( self.Cargo ) - - self:__SelectAction( 1 ) - - -- TODO:I need to find a more decent solution for this. - Task:E( { CargoPickedUp = Task.CargoPickedUp } ) - if self.Cargo:IsAlive() then - if Task.CargoPickedUp then - Task:CargoPickedUp( TaskUnit, self.Cargo ) - end - end - - end - - - --- - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_Cargo#TASK_CARGO Task - -- @param From - -- @param Event - -- @param To - -- @param Cargo - -- @param Core.Zone#ZONE_BASE DeployZone - function Fsm:onafterPrepareUnBoarding( TaskUnit, Task, From, Event, To, Cargo ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID(), From, Event, To, Cargo } ) - - self.Cargo = Cargo - self.DeployZone = nil - - -- Check if the Cargo is at a deployzone... If it is, provide it as a parameter! - if Cargo:IsAlive() then - for DeployZoneName, DeployZone in pairs( Task.DeployZones ) do - if Cargo:IsInZone( DeployZone ) then - self.DeployZone = DeployZone -- Core.Zone#ZONE_BASE - break - end - end - self:__UnBoard( -0.1, Cargo, self.DeployZone ) - end - end - - --- - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_Cargo#TASK_CARGO Task - -- @param From - -- @param Event - -- @param To - -- @param Cargo - -- @param Core.Zone#ZONE_BASE DeployZone - function Fsm:onafterUnBoard( TaskUnit, Task, From, Event, To, Cargo, DeployZone ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID(), From, Event, To, Cargo, DeployZone } ) - - function self.Cargo:OnEnterUnLoaded( From, Event, To, DeployZone, TaskProcess ) - self:E({From, Event, To, DeployZone, TaskProcess }) - TaskProcess:__UnBoarded( -0.1 ) - end - - if self.Cargo:IsAlive() then - self.Cargo:MessageToGroup( "UnBoarding ...", TaskUnit:GetGroup() ) - if DeployZone then - self.Cargo:UnBoard( DeployZone:GetPointVec2(), 400, self ) - else - self.Cargo:UnBoard( TaskUnit:GetPointVec2():AddX(60), 400, self ) - end - end - end - - - --- - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:onafterUnBoarded( TaskUnit, Task ) - - local TaskUnitName = TaskUnit:GetName() - self:E( { TaskUnit = TaskUnitName, Task = Task and Task:GetClassNameAndID() } ) - - self.Cargo:MessageToGroup( "UnBoarded ...", TaskUnit:GetGroup() ) - - TaskUnit:RemoveCargo( self.Cargo ) - - local NotInDeployZones = true - for DeployZoneName, DeployZone in pairs( Task.DeployZones ) do - if self.Cargo:IsInZone( DeployZone ) then - NotInDeployZones = false - end - end - - if NotInDeployZones == false then - self.Cargo:SetDeployed( true ) - end - - -- TODO:I need to find a more decent solution for this. - Task:E( { CargoDeployed = Task.CargoDeployed and "true" or "false" } ) - Task:E( { CargoIsAlive = self.Cargo:IsAlive() and "true" or "false" } ) - if self.Cargo:IsAlive() then - if Task.CargoDeployed then - Task:CargoDeployed( TaskUnit, self.Cargo, self.DeployZone ) - end - end - - self:__SelectAction( 1 ) - end - - - return self - - end - - - --- Set a limit on the amount of cargo items that can be loaded into the Carriers. - -- @param #TASK_CARGO self - -- @param CargoLimit Specifies a number of cargo items that can be loaded in the helicopter. - -- @return #TASK_CARGO - function TASK_CARGO:SetCargoLimit( CargoLimit ) - self.CargoLimit = CargoLimit - return self - end - - - ---@param Color Might be SMOKECOLOR.Blue, SMOKECOLOR.Red SMOKECOLOR.Orange, SMOKECOLOR.White or SMOKECOLOR.Green - function TASK_CARGO:SetSmokeColor(SmokeColor) - -- Makes sure Coloe is set - if SmokeColor == nil then - self.SmokeColor = SMOKECOLOR.Red -- Make sure a default color is exist - - elseif type(SmokeColor) == "number" then - self:F2(SmokeColor) - if SmokeColor > 0 and SmokeColor <=5 then -- Make sure number is within ragne, assuming first enum is one - self.SmokeColor = SMOKECOLOR.SmokeColor - end - end - end - - --@return SmokeColor - function TASK_CARGO:GetSmokeColor() - return self.SmokeColor - end - - --- @param #TASK_CARGO self - function TASK_CARGO:GetPlannedMenuText() - return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" - end - - --- @param #TASK_CARGO self - -- @return Core.Set#SET_CARGO The Cargo Set. - function TASK_CARGO:GetCargoSet() - - return self.SetCargo - end - - --- @param #TASK_CARGO self - -- @return #list The Deployment Zones. - function TASK_CARGO:GetDeployZones() - - return self.DeployZones - end - - --- @param #TASK_CARGO self - -- @param AI.AI_Cargo#AI_CARGO Cargo The cargo. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_CARGO - function TASK_CARGO:SetCargoPickup( Cargo, TaskUnit ) - - self:F({Cargo, TaskUnit}) - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteCargo = ProcessUnit:GetProcess( "RoutingToPickup", "RouteToPickupPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT - ActRouteCargo:Reset() - ActRouteCargo:SetCoordinate( Cargo:GetCoordinate() ) - ActRouteCargo:SetRange( Cargo:GetBoardingRange() ) - ActRouteCargo:SetMenuCancel( TaskUnit:GetGroup(), "Cancel Routing to Cargo " .. Cargo:GetName(), TaskUnit.Menu ) - ActRouteCargo:Start() - return self - end - - - --- @param #TASK_CARGO self - -- @param Core.Zone#ZONE DeployZone - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_CARGO - function TASK_CARGO:SetDeployZone( DeployZone, TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteDeployZone = ProcessUnit:GetProcess( "RoutingToDeploy", "RouteToDeployZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE - ActRouteDeployZone:Reset() - ActRouteDeployZone:SetZone( DeployZone ) - ActRouteDeployZone:SetMenuCancel( TaskUnit:GetGroup(), "Cancel Routing to Deploy Zone" .. DeployZone:GetName(), TaskUnit.Menu ) - ActRouteDeployZone:Start() - return self - end - - - --- @param #TASK_CARGO self - -- @param Core.Zone#ZONE DeployZone - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_CARGO - function TASK_CARGO:AddDeployZone( DeployZone, TaskUnit ) - - self.DeployZones[DeployZone:GetName()] = DeployZone - - return self - end - - --- @param #TASK_CARGO self - -- @param Core.Zone#ZONE DeployZone - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_CARGO - function TASK_CARGO:RemoveDeployZone( DeployZone, TaskUnit ) - - self.DeployZones[DeployZone:GetName()] = nil - - return self - end - - --- @param #TASK_CARGO self - -- @param @list DeployZones - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_CARGO - function TASK_CARGO:SetDeployZones( DeployZones, TaskUnit ) - - for DeployZoneID, DeployZone in pairs( DeployZones ) do - self.DeployZones[DeployZone:GetName()] = DeployZone - end - - return self - end - - - - --- @param #TASK_CARGO self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return Core.Zone#ZONE_BASE The Zone object where the Target is located on the map. - function TASK_CARGO:GetTargetZone( TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE - return ActRouteTarget:GetZone() - end - - --- Set a score when a target in scope of the A2G attack, has been destroyed . - -- @param #TASK_CARGO self - -- @param #string Text The text to display to the player, when the target has been destroyed. - -- @param #number Score The score in points. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_CARGO - function TASK_CARGO:SetScoreOnProgress( Text, Score, TaskUnit ) - self:F( { Text, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScoreProcess( "Engaging", "Account", "Account", Text, Score ) - - return self - end - - --- Set a score when all the targets in scope of the A2G attack, have been destroyed. - -- @param #TASK_CARGO self - -- @param #string Text The text to display to the player, when all targets hav been destroyed. - -- @param #number Score The score in points. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_CARGO - function TASK_CARGO:SetScoreOnSuccess( Text, Score, TaskUnit ) - self:F( { Text, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScore( "Success", Text, Score ) - - return self - end - - --- Set a penalty when the A2G attack has failed. - -- @param #TASK_CARGO self - -- @param #string Text The text to display to the player, when the A2G attack has failed. - -- @param #number Penalty The penalty in points. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_CARGO - function TASK_CARGO:SetScoreOnFail( Text, Penalty, TaskUnit ) - self:F( { Text, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScore( "Failed", Text, Penalty ) - - return self - end - - function TASK_CARGO:SetGoalTotal() - - self.GoalTotal = self.SetCargo:Count() - end - - function TASK_CARGO:GetGoalTotal() - - return self.GoalTotal - end - - -end - - -do -- TASK_CARGO_TRANSPORT - - --- The TASK_CARGO_TRANSPORT class - -- @type TASK_CARGO_TRANSPORT - -- @extends #TASK_CARGO - TASK_CARGO_TRANSPORT = { - ClassName = "TASK_CARGO_TRANSPORT", - } - - --- Instantiates a new TASK_CARGO_TRANSPORT. - -- @param #TASK_CARGO_TRANSPORT self - -- @param Tasking.Mission#MISSION Mission - -- @param Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. - -- @param #string TaskName The name of the Task. - -- @param Core.Set#SET_CARGO SetCargo The scope of the cargo to be transported. - -- @param #string TaskBriefing The Cargo Task briefing. - -- @return #TASK_CARGO_TRANSPORT self - function TASK_CARGO_TRANSPORT:New( Mission, SetGroup, TaskName, SetCargo, TaskBriefing ) - local self = BASE:Inherit( self, TASK_CARGO:New( Mission, SetGroup, TaskName, SetCargo, "Transport", TaskBriefing ) ) -- #TASK_CARGO_TRANSPORT - self:F() - - Mission:AddTask( self ) - - - -- Events - - self:AddTransition( "*", "CargoPickedUp", "*" ) - self:AddTransition( "*", "CargoDeployed", "*" ) - - self:E( { CargoDeployed = self.CargoDeployed ~= nil and "true" or "false" } ) - - --- OnBefore Transition Handler for Event CargoPickedUp. - -- @function [parent=#TASK_CARGO_TRANSPORT] OnBeforeCargoPickedUp - -- @param #TASK_CARGO_TRANSPORT self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that PickedUp the cargo. You can use this to retrieve the PlayerName etc. - -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event CargoPickedUp. - -- @function [parent=#TASK_CARGO_TRANSPORT] OnAfterCargoPickedUp - -- @param #TASK_CARGO_TRANSPORT self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that PickedUp the cargo. You can use this to retrieve the PlayerName etc. - -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. - - --- Synchronous Event Trigger for Event CargoPickedUp. - -- @function [parent=#TASK_CARGO_TRANSPORT] CargoPickedUp - -- @param #TASK_CARGO_TRANSPORT self - -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that PickedUp the cargo. You can use this to retrieve the PlayerName etc. - -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. - - --- Asynchronous Event Trigger for Event CargoPickedUp. - -- @function [parent=#TASK_CARGO_TRANSPORT] __CargoPickedUp - -- @param #TASK_CARGO_TRANSPORT self - -- @param #number Delay The delay in seconds. - -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that PickedUp the cargo. You can use this to retrieve the PlayerName etc. - -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. - - --- OnBefore Transition Handler for Event CargoDeployed. - -- @function [parent=#TASK_CARGO_TRANSPORT] OnBeforeCargoDeployed - -- @param #TASK_CARGO_TRANSPORT self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that Deployed the cargo. You can use this to retrieve the PlayerName etc. - -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. - -- @param Core.Zone#ZONE DeployZone The zone where the Cargo got Deployed or UnBoarded. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event CargoDeployed. - -- @function [parent=#TASK_CARGO_TRANSPORT] OnAfterCargoDeployed - -- @param #TASK_CARGO_TRANSPORT self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that Deployed the cargo. You can use this to retrieve the PlayerName etc. - -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. - -- @param Core.Zone#ZONE DeployZone The zone where the Cargo got Deployed or UnBoarded. - - --- Synchronous Event Trigger for Event CargoDeployed. - -- @function [parent=#TASK_CARGO_TRANSPORT] CargoDeployed - -- @param #TASK_CARGO_TRANSPORT self - -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that Deployed the cargo. You can use this to retrieve the PlayerName etc. - -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. - -- @param Core.Zone#ZONE DeployZone The zone where the Cargo got Deployed or UnBoarded. - - --- Asynchronous Event Trigger for Event CargoDeployed. - -- @function [parent=#TASK_CARGO_TRANSPORT] __CargoDeployed - -- @param #TASK_CARGO_TRANSPORT self - -- @param #number Delay The delay in seconds. - -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that Deployed the cargo. You can use this to retrieve the PlayerName etc. - -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. - -- @param Core.Zone#ZONE DeployZone The zone where the Cargo got Deployed or UnBoarded. - - local Fsm = self:GetUnitProcess() - - local CargoReport = REPORT:New( "Transport Cargo. The following cargo needs to be transported including initial positions:") - - SetCargo:ForEachCargo( - --- @param Core.Cargo#CARGO Cargo - function( Cargo ) - local CargoType = Cargo:GetType() - local CargoName = Cargo:GetName() - local CargoCoordinate = Cargo:GetCoordinate() - CargoReport:Add( string.format( '- "%s" (%s) at %s', CargoName, CargoType, CargoCoordinate:ToStringMGRS() ) ) - end - ) - - self:SetBriefing( - TaskBriefing or - CargoReport:Text() - ) - - - return self - end - - function TASK_CARGO_TRANSPORT:ReportOrder( ReportGroup ) - - return true - end - - - --- - -- @param #TASK_CARGO_TRANSPORT self - -- @return #boolean - function TASK_CARGO_TRANSPORT:IsAllCargoTransported() - - local CargoSet = self:GetCargoSet() - local Set = CargoSet:GetSet() - - local DeployZones = self:GetDeployZones() - - local CargoDeployed = true - - -- Loop the CargoSet (so evaluate each Cargo in the SET_CARGO ). - for CargoID, CargoData in pairs( Set ) do - local Cargo = CargoData -- Core.Cargo#CARGO - - if Cargo:IsDeployed() then - - -- Loop the DeployZones set for the TASK_CARGO_TRANSPORT. - for DeployZoneID, DeployZone in pairs( DeployZones ) do - - -- If there is a Cargo not in one of DeployZones, then not all Cargo is deployed. - self:T( { Cargo.CargoObject } ) - if Cargo:IsInZone( DeployZone ) == false then - CargoDeployed = false - end - end - else - CargoDeployed = false - end - end - - return CargoDeployed - end - - --- @param #TASK_CARGO_TRANSPORT self - function TASK_CARGO_TRANSPORT:onafterGoal( TaskUnit, From, Event, To ) - local CargoSet = self.CargoSet - - if self:IsAllCargoTransported() then - self:Success() - end - - self:__Goal( -10 ) - end - -end - --- The order of the declarations is important here. Don't touch it. - ---- Declare the event dispatcher based on the EVENT class -_EVENTDISPATCHER = EVENT:New() -- Core.Event#EVENT - ---- Declare the timer dispatcher based on the SCHEDULEDISPATCHER class -_SCHEDULEDISPATCHER = SCHEDULEDISPATCHER:New() -- Core.Timer#SCHEDULEDISPATCHER - ---- Declare the main database object, which is used internally by the MOOSE classes. -_DATABASE = DATABASE:New() -- Core.Database#DATABASE - -_SETTINGS = SETTINGS:Set() - -BASE:TraceOnOff( false ) + env.info( "Moose: " .. IncludeFile .. " dynamically loaded from " .. __Moose.ProgramPath ) + return f() + end + end +end + +__Moose.ProgramPath = "Scripts/Moose/" + +__Moose.Includes = {} +__Moose.Include( 'Utilities/Routines.lua' ) +__Moose.Include( 'Utilities/Utils.lua' ) +__Moose.Include( 'Core/Base.lua' ) +__Moose.Include( 'Core/Report.lua' ) +__Moose.Include( 'Core/Scheduler.lua' ) +__Moose.Include( 'Core/ScheduleDispatcher.lua' ) +__Moose.Include( 'Core/Event.lua' ) +__Moose.Include( 'Core/Settings.lua' ) +__Moose.Include( 'Core/Menu.lua' ) +__Moose.Include( 'Core/Zone.lua' ) +__Moose.Include( 'Core/Database.lua' ) +__Moose.Include( 'Core/Set.lua' ) +__Moose.Include( 'Core/Point.lua' ) +__Moose.Include( 'Core/Message.lua' ) +__Moose.Include( 'Core/Fsm.lua' ) +__Moose.Include( 'Core/Radio.lua' ) +__Moose.Include( 'Core/SpawnStatic.lua' ) +__Moose.Include( 'Core/Cargo.lua' ) +__Moose.Include( 'Core/Spot.lua' ) +__Moose.Include( 'Wrapper/Object.lua' ) +__Moose.Include( 'Wrapper/Identifiable.lua' ) +__Moose.Include( 'Wrapper/Positionable.lua' ) +__Moose.Include( 'Wrapper/Controllable.lua' ) +__Moose.Include( 'Wrapper/Group.lua' ) +__Moose.Include( 'Wrapper/Unit.lua' ) +__Moose.Include( 'Wrapper/Client.lua' ) +__Moose.Include( 'Wrapper/Static.lua' ) +__Moose.Include( 'Wrapper/Airbase.lua' ) +__Moose.Include( 'Wrapper/Scenery.lua' ) +__Moose.Include( 'Functional/Scoring.lua' ) +__Moose.Include( 'Functional/CleanUp.lua' ) +__Moose.Include( 'Functional/Spawn.lua' ) +__Moose.Include( 'Functional/Movement.lua' ) +__Moose.Include( 'Functional/Sead.lua' ) +__Moose.Include( 'Functional/Escort.lua' ) +__Moose.Include( 'Functional/MissileTrainer.lua' ) +__Moose.Include( 'Functional/AirbasePolice.lua' ) +__Moose.Include( 'Functional/Detection.lua' ) +__Moose.Include( 'Functional/Designate.lua' ) +__Moose.Include( 'Functional/RAT.lua' ) +__Moose.Include( 'AI/AI_Balancer.lua' ) +__Moose.Include( 'AI/AI_A2A.lua' ) +__Moose.Include( 'AI/AI_A2A_Patrol.lua' ) +__Moose.Include( 'AI/AI_A2A_Cap.lua' ) +__Moose.Include( 'AI/AI_A2A_Gci.lua' ) +__Moose.Include( 'AI/AI_A2A_Dispatcher.lua' ) +__Moose.Include( 'AI/AI_Patrol.lua' ) +__Moose.Include( 'AI/AI_Cap.lua' ) +__Moose.Include( 'AI/AI_Cas.lua' ) +__Moose.Include( 'AI/AI_Bai.lua' ) +__Moose.Include( 'AI/AI_Formation.lua' ) +__Moose.Include( 'Actions/Act_Assign.lua' ) +__Moose.Include( 'Actions/Act_Route.lua' ) +__Moose.Include( 'Actions/Act_Account.lua' ) +__Moose.Include( 'Actions/Act_Assist.lua' ) +__Moose.Include( 'Tasking/CommandCenter.lua' ) +__Moose.Include( 'Tasking/Mission.lua' ) +__Moose.Include( 'Tasking/Task.lua' ) +__Moose.Include( 'Tasking/DetectionManager.lua' ) +__Moose.Include( 'Tasking/Task_A2G_Dispatcher.lua' ) +__Moose.Include( 'Tasking/Task_A2G.lua' ) +__Moose.Include( 'Tasking/Task_A2A_Dispatcher.lua' ) +__Moose.Include( 'Tasking/Task_A2A.lua' ) +__Moose.Include( 'Tasking/Task_Cargo.lua' ) +__Moose.Include( 'Moose.lua' ) +BASE:TraceOnOff( true ) env.info( '*** MOOSE INCLUDE END *** ' ) diff --git a/Moose Mission Setup/Moose_.lua b/Moose Mission Setup/Moose_.lua index 24bc41e6d..85e5d34e9 100644 --- a/Moose Mission Setup/Moose_.lua +++ b/Moose Mission Setup/Moose_.lua @@ -1,26303 +1,85 @@ -env.info('*** MOOSE STATIC INCLUDE START *** ') -env.info('Moose Generation Timestamp: 20171006_1228') -env.setErrorMessageBoxEnabled(false) -routines={} -routines.majorVersion=3 -routines.minorVersion=3 -routines.build=22 -routines.utils={} -routines.utils.deepCopy=function(object) -local lookup_table={} -local function _copy(object) -if type(object)~="table"then -return object -elseif lookup_table[object]then -return lookup_table[object] -end -local new_table={} -lookup_table[object]=new_table -for index,value in pairs(object)do -new_table[_copy(index)]=_copy(value) -end -return setmetatable(new_table,getmetatable(object)) -end -local objectreturn=_copy(object) -return objectreturn -end -routines.utils.oneLineSerialize=function(tbl) -lookup_table={} -local function _Serialize(tbl) -if type(tbl)=='table'then -if lookup_table[tbl]then -return lookup_table[object] -end -local tbl_str={} -lookup_table[tbl]=tbl_str -tbl_str[#tbl_str+1]='{' -for ind,val in pairs(tbl)do -local ind_str={} -if type(ind)=="number"then -ind_str[#ind_str+1]='[' -ind_str[#ind_str+1]=tostring(ind) -ind_str[#ind_str+1]=']=' -else -ind_str[#ind_str+1]='[' -ind_str[#ind_str+1]=routines.utils.basicSerialize(ind) -ind_str[#ind_str+1]=']=' -end -local val_str={} -if((type(val)=='number')or(type(val)=='boolean'))then -val_str[#val_str+1]=tostring(val) -val_str[#val_str+1]=',' -tbl_str[#tbl_str+1]=table.concat(ind_str) -tbl_str[#tbl_str+1]=table.concat(val_str) -elseif type(val)=='string'then -val_str[#val_str+1]=routines.utils.basicSerialize(val) -val_str[#val_str+1]=',' -tbl_str[#tbl_str+1]=table.concat(ind_str) -tbl_str[#tbl_str+1]=table.concat(val_str) -elseif type(val)=='nil'then -val_str[#val_str+1]='nil,' -tbl_str[#tbl_str+1]=table.concat(ind_str) -tbl_str[#tbl_str+1]=table.concat(val_str) -elseif type(val)=='table'then -if ind=="__index"then -else -val_str[#val_str+1]=_Serialize(val) -val_str[#val_str+1]=',' -tbl_str[#tbl_str+1]=table.concat(ind_str) -tbl_str[#tbl_str+1]=table.concat(val_str) -end -elseif type(val)=='function'then -else -end -end -tbl_str[#tbl_str+1]='}' -return table.concat(tbl_str) -else -return tostring(tbl) -end -end -local objectreturn=_Serialize(tbl) -return objectreturn -end -routines.utils.basicSerialize=function(s) -if s==nil then -return"\"\"" -else -if((type(s)=='number')or(type(s)=='boolean')or(type(s)=='function')or(type(s)=='table')or(type(s)=='userdata'))then -return tostring(s) -elseif type(s)=='string'then -s=string.format('%q',s) -return s -end -end -end -routines.utils.toDegree=function(angle) -return angle*180/math.pi -end -routines.utils.toRadian=function(angle) -return angle*math.pi/180 -end -routines.utils.metersToNM=function(meters) -return meters/1852 -end -routines.utils.metersToFeet=function(meters) -return meters/0.3048 -end -routines.utils.NMToMeters=function(NM) -return NM*1852 -end -routines.utils.feetToMeters=function(feet) -return feet*0.3048 -end -routines.utils.mpsToKnots=function(mps) -return mps*3600/1852 -end -routines.utils.mpsToKmph=function(mps) -return mps*3.6 -end -routines.utils.knotsToMps=function(knots) -return knots*1852/3600 -end -routines.utils.kmphToMps=function(kmph) -return kmph/3.6 -end -function routines.utils.makeVec2(Vec3) -if Vec3.z then -return{x=Vec3.x,y=Vec3.z} -else -return{x=Vec3.x,y=Vec3.y} -end -end -function routines.utils.makeVec3(Vec2,y) -if not Vec2.z then -if not y then -y=0 -end -return{x=Vec2.x,y=y,z=Vec2.y} -else -return{x=Vec2.x,y=Vec2.y,z=Vec2.z} -end -end -function routines.utils.makeVec3GL(Vec2,offset) -local adj=offset or 0 -if not Vec2.z then -return{x=Vec2.x,y=(land.getHeight(Vec2)+adj),z=Vec2.y} -else -return{x=Vec2.x,y=(land.getHeight({x=Vec2.x,y=Vec2.z})+adj),z=Vec2.z} -end -end -routines.utils.zoneToVec3=function(zone) -local new={} -if type(zone)=='table'and zone.point then -new.x=zone.point.x -new.y=zone.point.y -new.z=zone.point.z -return new -elseif type(zone)=='string'then -zone=trigger.misc.getZone(zone) -if zone then -new.x=zone.point.x -new.y=zone.point.y -new.z=zone.point.z -return new -end -end -end -function routines.utils.getDir(vec,point) -local dir=math.atan2(vec.z,vec.x) -dir=dir+routines.getNorthCorrection(point) -if dir<0 then -dir=dir+2*math.pi -end -return dir -end -function routines.utils.get2DDist(point1,point2) -point1=routines.utils.makeVec3(point1) -point2=routines.utils.makeVec3(point2) -return routines.vec.mag({x=point1.x-point2.x,y=0,z=point1.z-point2.z}) -end -function routines.utils.get3DDist(point1,point2) -return routines.vec.mag({x=point1.x-point2.x,y=point1.y-point2.y,z=point1.z-point2.z}) -end -routines.vec={} -routines.vec.add=function(vec1,vec2) -return{x=vec1.x+vec2.x,y=vec1.y+vec2.y,z=vec1.z+vec2.z} -end -routines.vec.sub=function(vec1,vec2) -return{x=vec1.x-vec2.x,y=vec1.y-vec2.y,z=vec1.z-vec2.z} -end -routines.vec.scalarMult=function(vec,mult) -return{x=vec.x*mult,y=vec.y*mult,z=vec.z*mult} -end -routines.vec.scalar_mult=routines.vec.scalarMult -routines.vec.dp=function(vec1,vec2) -return vec1.x*vec2.x+vec1.y*vec2.y+vec1.z*vec2.z -end -routines.vec.cp=function(vec1,vec2) -return{x=vec1.y*vec2.z-vec1.z*vec2.y,y=vec1.z*vec2.x-vec1.x*vec2.z,z=vec1.x*vec2.y-vec1.y*vec2.x} -end -routines.vec.mag=function(vec) -return(vec.x^2+vec.y^2+vec.z^2)^0.5 -end -routines.vec.getUnitVec=function(vec) -local mag=routines.vec.mag(vec) -return{x=vec.x/mag,y=vec.y/mag,z=vec.z/mag} -end -routines.vec.rotateVec2=function(vec2,theta) -return{x=vec2.x*math.cos(theta)-vec2.y*math.sin(theta),y=vec2.x*math.sin(theta)+vec2.y*math.cos(theta)} -end -routines.tostringMGRS=function(MGRS,acc) -if acc==0 then -return MGRS.UTMZone..' '..MGRS.MGRSDigraph -else -return MGRS.UTMZone..' '..MGRS.MGRSDigraph..' '..string.format('%0'..acc..'d',routines.utils.round(MGRS.Easting/(10^(5-acc)),0)) -..' '..string.format('%0'..acc..'d',routines.utils.round(MGRS.Northing/(10^(5-acc)),0)) -end -end -routines.tostringLL=function(lat,lon,acc,DMS) -local latHemi,lonHemi -if lat>0 then -latHemi='N' -else -latHemi='S' -end -if lon>0 then -lonHemi='E' -else -lonHemi='W' -end -lat=math.abs(lat) -lon=math.abs(lon) -local latDeg=math.floor(lat) -local latMin=(lat-latDeg)*60 -local lonDeg=math.floor(lon) -local lonMin=(lon-lonDeg)*60 -if DMS then -local oldLatMin=latMin -latMin=math.floor(latMin) -local latSec=routines.utils.round((oldLatMin-latMin)*60,acc) -local oldLonMin=lonMin -lonMin=math.floor(lonMin) -local lonSec=routines.utils.round((oldLonMin-lonMin)*60,acc) -if latSec==60 then -latSec=0 -latMin=latMin+1 -end -if lonSec==60 then -lonSec=0 -lonMin=lonMin+1 -end -local secFrmtStr -if acc<=0 then -secFrmtStr='%02d' -else -local width=3+acc -secFrmtStr='%0'..width..'.'..acc..'f' -end -return string.format('%02d',latDeg)..' '..string.format('%02d',latMin)..'\' '..string.format(secFrmtStr,latSec)..'"'..latHemi..' ' -..string.format('%02d',lonDeg)..' '..string.format('%02d',lonMin)..'\' '..string.format(secFrmtStr,lonSec)..'"'..lonHemi -else -latMin=routines.utils.round(latMin,acc) -lonMin=routines.utils.round(lonMin,acc) -if latMin==60 then -latMin=0 -latDeg=latDeg+1 -end -if lonMin==60 then -lonMin=0 -lonDeg=lonDeg+1 -end -local minFrmtStr -if acc<=0 then -minFrmtStr='%02d' -else -local width=3+acc -minFrmtStr='%0'..width..'.'..acc..'f' -end -return string.format('%02d',latDeg)..' '..string.format(minFrmtStr,latMin)..'\''..latHemi..' ' -..string.format('%02d',lonDeg)..' '..string.format(minFrmtStr,lonMin)..'\''..lonHemi -end -end -routines.tostringBR=function(az,dist,alt,metric) -az=routines.utils.round(routines.utils.toDegree(az),0) -if metric then -dist=routines.utils.round(dist/1000,2) -else -dist=routines.utils.round(routines.utils.metersToNM(dist),2) -end -local s=string.format('%03d',az)..' for '..dist -if alt then -if metric then -s=s..' at '..routines.utils.round(alt,0) -else -s=s..' at '..routines.utils.round(routines.utils.metersToFeet(alt),0) -end -end -return s -end -routines.getNorthCorrection=function(point) -if not point.z then -point.z=point.y -point.y=0 -end -local lat,lon=coord.LOtoLL(point) -local north_posit=coord.LLtoLO(lat+1,lon) -return math.atan2(north_posit.z-point.z,north_posit.x-point.x) -end -do -local idNum=0 -routines.addEventHandler=function(f) -local handler={} -idNum=idNum+1 -handler.id=idNum -handler.f=f -handler.onEvent=function(self,event) -self.f(event) -end -world.addEventHandler(handler) -end -routines.removeEventHandler=function(id) -for key,handler in pairs(world.eventHandlers)do -if handler.id and handler.id==id then -world.eventHandlers[key]=nil -return true -end -end -return false -end -end -function routines.getRandPointInCircle(point,radius,innerRadius) -local theta=2*math.pi*math.random() -local rad=math.random()+math.random() -if rad>1 then -rad=2-rad -end -local radMult -if innerRadius and innerRadius<=radius then -radMult=(radius-innerRadius)*rad+innerRadius -else -radMult=radius*rad -end -if not point.z then -point.z=point.y -end -local rndCoord -if radius>0 then -rndCoord={x=math.cos(theta)*radMult+point.x,y=math.sin(theta)*radMult+point.z} -else -rndCoord={x=point.x,y=point.z} -end -return rndCoord -end -routines.goRoute=function(group,path) -local misTask={ -id='Mission', -params={ -route={ -points=routines.utils.deepCopy(path), -}, -}, -} -if type(group)=='string'then -group=Group.getByName(group) -end -local groupCon=group:getController() -if groupCon then -groupCon:setTask(misTask) -return true -end -Controller.setTask(groupCon,misTask) -return false -end -routines.ground={} -routines.fixedWing={} -routines.heli={} -routines.ground.buildWP=function(point,overRideForm,overRideSpeed) -local wp={} -wp.x=point.x -if point.z then -wp.y=point.z -else -wp.y=point.y -end -local form,speed -if point.speed and not overRideSpeed then -wp.speed=point.speed -elseif type(overRideSpeed)=='number'then -wp.speed=overRideSpeed -else -wp.speed=routines.utils.kmphToMps(20) -end -if point.form and not overRideForm then -form=point.form -else -form=overRideForm -end -if not form then -wp.action='Cone' -else -form=string.lower(form) -if form=='off_road'or form=='off road'then -wp.action='Off Road' -elseif form=='on_road'or form=='on road'then -wp.action='On Road' -elseif form=='rank'or form=='line_abrest'or form=='line abrest'or form=='lineabrest'then -wp.action='Rank' -elseif form=='cone'then -wp.action='Cone' -elseif form=='diamond'then -wp.action='Diamond' -elseif form=='vee'then -wp.action='Vee' -elseif form=='echelon_left'or form=='echelon left'or form=='echelonl'then -wp.action='EchelonL' -elseif form=='echelon_right'or form=='echelon right'or form=='echelonr'then -wp.action='EchelonR' -else -wp.action='Cone' -end -end -wp.type='Turning Point' -return wp -end -routines.fixedWing.buildWP=function(point,WPtype,speed,alt,altType) -local wp={} -wp.x=point.x -if point.z then -wp.y=point.z -else -wp.y=point.y -end -if alt and type(alt)=='number'then -wp.alt=alt -else -wp.alt=2000 -end -if altType then -altType=string.lower(altType) -if altType=='radio'or'agl'then -wp.alt_type='RADIO' -elseif altType=='baro'or'asl'then -wp.alt_type='BARO' -end -else -wp.alt_type='RADIO' -end -if point.speed then -speed=point.speed -end -if point.type then -WPtype=point.type -end -if not speed then -wp.speed=routines.utils.kmphToMps(500) -else -wp.speed=speed -end -if not WPtype then -wp.action='Turning Point' -else -WPtype=string.lower(WPtype) -if WPtype=='flyover'or WPtype=='fly over'or WPtype=='fly_over'then -wp.action='Fly Over Point' -elseif WPtype=='turningpoint'or WPtype=='turning point'or WPtype=='turning_point'then -wp.action='Turning Point' -else -wp.action='Turning Point' -end -end -wp.type='Turning Point' -return wp -end -routines.heli.buildWP=function(point,WPtype,speed,alt,altType) -local wp={} -wp.x=point.x -if point.z then -wp.y=point.z -else -wp.y=point.y -end -if alt and type(alt)=='number'then -wp.alt=alt -else -wp.alt=500 -end -if altType then -altType=string.lower(altType) -if altType=='radio'or'agl'then -wp.alt_type='RADIO' -elseif altType=='baro'or'asl'then -wp.alt_type='BARO' -end -else -wp.alt_type='RADIO' -end -if point.speed then -speed=point.speed -end -if point.type then -WPtype=point.type -end -if not speed then -wp.speed=routines.utils.kmphToMps(200) -else -wp.speed=speed -end -if not WPtype then -wp.action='Turning Point' -else -WPtype=string.lower(WPtype) -if WPtype=='flyover'or WPtype=='fly over'or WPtype=='fly_over'then -wp.action='Fly Over Point' -elseif WPtype=='turningpoint'or WPtype=='turning point'or WPtype=='turning_point'then -wp.action='Turning Point' -else -wp.action='Turning Point' -end -end -wp.type='Turning Point' -return wp -end -routines.groupToRandomPoint=function(vars) -local group=vars.group -local point=vars.point -local radius=vars.radius or 0 -local innerRadius=vars.innerRadius -local form=vars.form or'Cone' -local heading=vars.heading or math.random()*2*math.pi -local headingDegrees=vars.headingDegrees -local speed=vars.speed or routines.utils.kmphToMps(20) -local useRoads -if not vars.disableRoads then -useRoads=true -else -useRoads=false -end -local path={} -if headingDegrees then -heading=headingDegrees*math.pi/180 -end -if heading>=2*math.pi then -heading=heading-2*math.pi -end -local rndCoord=routines.getRandPointInCircle(point,radius,innerRadius) -local offset={} -local posStart=routines.getLeadPos(group) -offset.x=routines.utils.round(math.sin(heading-(math.pi/2))*50+rndCoord.x,3) -offset.z=routines.utils.round(math.cos(heading+(math.pi/2))*50+rndCoord.y,3) -path[#path+1]=routines.ground.buildWP(posStart,form,speed) -if useRoads==true and((point.x-posStart.x)^2+(point.z-posStart.z)^2)^0.5>radius*1.3 then -path[#path+1]=routines.ground.buildWP({['x']=posStart.x+11,['z']=posStart.z+11},'off_road',speed) -path[#path+1]=routines.ground.buildWP(posStart,'on_road',speed) -path[#path+1]=routines.ground.buildWP(offset,'on_road',speed) -else -path[#path+1]=routines.ground.buildWP({['x']=posStart.x+25,['z']=posStart.z+25},form,speed) -end -path[#path+1]=routines.ground.buildWP(offset,form,speed) -path[#path+1]=routines.ground.buildWP(rndCoord,form,speed) -routines.goRoute(group,path) -return -end -routines.groupRandomDistSelf=function(gpData,dist,form,heading,speed) -local pos=routines.getLeadPos(gpData) -local fakeZone={} -fakeZone.radius=dist or math.random(300,1000) -fakeZone.point={x=pos.x,y,pos.y,z=pos.z} -routines.groupToRandomZone(gpData,fakeZone,form,heading,speed) -return -end -routines.groupToRandomZone=function(gpData,zone,form,heading,speed) -if type(gpData)=='string'then -gpData=Group.getByName(gpData) -end -if type(zone)=='string'then -zone=trigger.misc.getZone(zone) -elseif type(zone)=='table'and not zone.radius then -zone=trigger.misc.getZone(zone[math.random(1,#zone)]) -end -if speed then -speed=routines.utils.kmphToMps(speed) -end -local vars={} -vars.group=gpData -vars.radius=zone.radius -vars.form=form -vars.headingDegrees=heading -vars.speed=speed -vars.point=routines.utils.zoneToVec3(zone) -routines.groupToRandomPoint(vars) -return -end -routines.isTerrainValid=function(coord,terrainTypes) -if coord.z then -coord.y=coord.z -end -local typeConverted={} -if type(terrainTypes)=='string'then -for constId,constData in pairs(land.SurfaceType)do -if string.lower(constId)==string.lower(terrainTypes)or string.lower(constData)==string.lower(terrainTypes)then -table.insert(typeConverted,constId) -end -end -elseif type(terrainTypes)=='table'then -for typeId,typeData in pairs(terrainTypes)do -for constId,constData in pairs(land.SurfaceType)do -if string.lower(constId)==string.lower(typeData)or string.lower(constData)==string.lower(typeId)then -table.insert(typeConverted,constId) -end -end -end -end -for validIndex,validData in pairs(typeConverted)do -if land.getSurfaceType(coord)==land.SurfaceType[validData]then -return true -end -end -return false -end -routines.groupToPoint=function(gpData,point,form,heading,speed,useRoads) -if type(point)=='string'then -point=trigger.misc.getZone(point) -end -if speed then -speed=routines.utils.kmphToMps(speed) -end -local vars={} -vars.group=gpData -vars.form=form -vars.headingDegrees=heading -vars.speed=speed -vars.disableRoads=useRoads -vars.point=routines.utils.zoneToVec3(point) -routines.groupToRandomPoint(vars) -return -end -routines.getLeadPos=function(group) -if type(group)=='string'then -group=Group.getByName(group) -end -local units=group:getUnits() -local leader=units[1] -if not leader then -local lowestInd=math.huge -for ind,unit in pairs(units)do -if ind0 then -local maxPos=-math.huge -local maxPosInd -for i=1,#unitPosTbl do -local rotatedVec2=routines.vec.rotateVec2(routines.utils.makeVec2(unitPosTbl[i]),heading) -if(not maxPos)or maxPos=1.0 then -CurrentZoneID=routines.IsUnitInZones(CargoUnit,LandingZones) -if CurrentZoneID then -break -end -end -end -end -return CurrentZoneID -end -function routines.IsUnitInZones(TransportUnit,LandingZones) -local TransportZoneResult=nil -local TransportZonePos=nil -local TransportZone=nil -if TransportUnit then -local TransportUnitPos=TransportUnit:getPosition().p -if type(LandingZones)=="table"then -for LandingZoneID,LandingZoneName in pairs(LandingZones)do -TransportZone=trigger.misc.getZone(LandingZoneName) -if TransportZone then -TransportZonePos={radius=TransportZone.radius,x=TransportZone.point.x,y=TransportZone.point.y,z=TransportZone.point.z} -if(((TransportUnitPos.x-TransportZonePos.x)^2+(TransportUnitPos.z-TransportZonePos.z)^2)^0.5<=TransportZonePos.radius)then -TransportZoneResult=LandingZoneID -break -end -end -end -else -TransportZone=trigger.misc.getZone(LandingZones) -TransportZonePos={radius=TransportZone.radius,x=TransportZone.point.x,y=TransportZone.point.y,z=TransportZone.point.z} -if(((TransportUnitPos.x-TransportZonePos.x)^2+(TransportUnitPos.z-TransportZonePos.z)^2)^0.5<=TransportZonePos.radius)then -TransportZoneResult=1 -end -end -if TransportZoneResult then -else -end -return TransportZoneResult -else -return nil -end -end -function routines.IsUnitNearZonesRadius(TransportUnit,LandingZones,ZoneRadius) -local TransportZoneResult=nil -local TransportZonePos=nil -local TransportZone=nil -if TransportUnit then -local TransportUnitPos=TransportUnit:getPosition().p -if type(LandingZones)=="table"then -for LandingZoneID,LandingZoneName in pairs(LandingZones)do -TransportZone=trigger.misc.getZone(LandingZoneName) -if TransportZone then -TransportZonePos={radius=TransportZone.radius,x=TransportZone.point.x,y=TransportZone.point.y,z=TransportZone.point.z} -if(((TransportUnitPos.x-TransportZonePos.x)^2+(TransportUnitPos.z-TransportZonePos.z)^2)^0.5<=ZoneRadius)then -TransportZoneResult=LandingZoneID -break -end -end -end -else -TransportZone=trigger.misc.getZone(LandingZones) -TransportZonePos={radius=TransportZone.radius,x=TransportZone.point.x,y=TransportZone.point.y,z=TransportZone.point.z} -if(((TransportUnitPos.x-TransportZonePos.x)^2+(TransportUnitPos.z-TransportZonePos.z)^2)^0.5<=ZoneRadius)then -TransportZoneResult=1 -end -end -if TransportZoneResult then -else -end -return TransportZoneResult -else -return nil -end -end -function routines.IsStaticInZones(TransportStatic,LandingZones) -local TransportZoneResult=nil -local TransportZonePos=nil -local TransportZone=nil -local TransportStaticPos=TransportStatic:getPosition().p -if type(LandingZones)=="table"then -for LandingZoneID,LandingZoneName in pairs(LandingZones)do -TransportZone=trigger.misc.getZone(LandingZoneName) -if TransportZone then -TransportZonePos={radius=TransportZone.radius,x=TransportZone.point.x,y=TransportZone.point.y,z=TransportZone.point.z} -if(((TransportStaticPos.x-TransportZonePos.x)^2+(TransportStaticPos.z-TransportZonePos.z)^2)^0.5<=TransportZonePos.radius)then -TransportZoneResult=LandingZoneID -break -end -end -end -else -TransportZone=trigger.misc.getZone(LandingZones) -TransportZonePos={radius=TransportZone.radius,x=TransportZone.point.x,y=TransportZone.point.y,z=TransportZone.point.z} -if(((TransportStaticPos.x-TransportZonePos.x)^2+(TransportStaticPos.z-TransportZonePos.z)^2)^0.5<=TransportZonePos.radius)then -TransportZoneResult=1 -end -end -return TransportZoneResult -end -function routines.IsUnitInRadius(CargoUnit,ReferencePosition,Radius) -local Valid=true -local CargoPos=CargoUnit:getPosition().p -local ReferenceP=ReferencePosition.p -if(((CargoPos.x-ReferenceP.x)^2+(CargoPos.z-ReferenceP.z)^2)^0.5<=Radius)then -else -Valid=false -end -return Valid -end -function routines.IsPartOfGroupInRadius(CargoGroup,ReferencePosition,Radius) -local Valid=true -Valid=routines.ValidateGroup(CargoGroup,"CargoGroup",Valid) -local CargoUnits=CargoGroup:getUnits() -for CargoUnitId,CargoUnit in pairs(CargoUnits)do -local CargoUnitPos=CargoUnit:getPosition().p -local ReferenceP=ReferencePosition.p -if(((CargoUnitPos.x-ReferenceP.x)^2+(CargoUnitPos.z-ReferenceP.z)^2)^0.5<=Radius)then -else -Valid=false -break -end -end -return Valid -end -function routines.ValidateString(Variable,VariableName,Valid) -if type(Variable)=="string"then -if Variable==""then -error("routines.ValidateString: error: "..VariableName.." must be filled out!") -Valid=false -end -else -error("routines.ValidateString: error: "..VariableName.." is not a string.") -Valid=false -end -return Valid -end -function routines.ValidateNumber(Variable,VariableName,Valid) -if type(Variable)=="number"then -else -error("routines.ValidateNumber: error: "..VariableName.." is not a number.") -Valid=false -end -return Valid -end -function routines.ValidateGroup(Variable,VariableName,Valid) -if Variable==nil then -error("routines.ValidateGroup: error: "..VariableName.." is a nil value!") -Valid=false -end -return Valid -end -function routines.ValidateZone(LandingZones,VariableName,Valid) -if LandingZones==nil then -error("routines.ValidateGroup: error: "..VariableName.." is a nil value!") -Valid=false -end -if type(LandingZones)=="table"then -for LandingZoneID,LandingZoneName in pairs(LandingZones)do -if trigger.misc.getZone(LandingZoneName)==nil then -error("routines.ValidateGroup: error: Zone "..LandingZoneName.." does not exist!") -Valid=false -break -end -end -else -if trigger.misc.getZone(LandingZones)==nil then -error("routines.ValidateGroup: error: Zone "..LandingZones.." does not exist!") -Valid=false -end -end -return Valid -end -function routines.ValidateEnumeration(Variable,VariableName,Enum,Valid) -local ValidVariable=false -for EnumId,EnumData in pairs(Enum)do -if Variable==EnumData then -ValidVariable=true -break -end -end -if ValidVariable then -else -error('TransportValidateEnum: " .. VariableName .. " is not a valid type.'..Variable) -Valid=false -end -return Valid -end -function routines.getGroupRoute(groupIdent,task) -local gpId=groupIdent -if type(groupIdent)=='string'and not tonumber(groupIdent)then -gpId=_DATABASE.Templates.Groups[groupIdent].groupId -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 -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 -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 -for group_num,group_data in pairs(obj_type_data.group)do -if group_data and group_data.groupId==gpId then -if group_data.route and group_data.route.points and#group_data.route.points>0 then -local points={} -for point_num,point in pairs(group_data.route.points)do -local routeData={} -if not point.point then -routeData.x=point.x -routeData.y=point.y -else -routeData.point=point.point -end -routeData.form=point.action -routeData.speed=point.speed -routeData.alt=point.alt -routeData.alt_type=point.alt_type -routeData.airdromeId=point.airdromeId -routeData.helipadId=point.helipadId -routeData.type=point.type -routeData.action=point.action -if task then -routeData.task=point.task -end -points[point_num]=routeData -end -return points -end -return -end -end -end -end -end -end -end -end -end -end -routines.ground.patrolRoute=function(vars) -local tempRoute={} -local useRoute={} -local gpData=vars.gpData -if type(gpData)=='string'then -gpData=Group.getByName(gpData) -end -local useGroupRoute -if not vars.useGroupRoute then -useGroupRoute=vars.gpData -else -useGroupRoute=vars.useGroupRoute -end -local routeProvided=false -if not vars.route then -if useGroupRoute then -tempRoute=routines.getGroupRoute(useGroupRoute) -end -else -useRoute=vars.route -local posStart=routines.getLeadPos(gpData) -useRoute[1]=routines.ground.buildWP(posStart,useRoute[1].action,useRoute[1].speed) -routeProvided=true -end -local overRideSpeed=vars.speed or'default' -local pType=vars.pType -local offRoadForm=vars.offRoadForm or'default' -local onRoadForm=vars.onRoadForm or'default' -if routeProvided==false and#tempRoute>0 then -local posStart=routines.getLeadPos(gpData) -useRoute[#useRoute+1]=routines.ground.buildWP(posStart,offRoadForm,overRideSpeed) -for i=1,#tempRoute do -local tempForm=tempRoute[i].action -local tempSpeed=tempRoute[i].speed -if offRoadForm=='default'then -tempForm=tempRoute[i].action -end -if onRoadForm=='default'then -onRoadForm='On Road' -end -if(string.lower(tempRoute[i].action)=='on road'or string.lower(tempRoute[i].action)=='onroad'or string.lower(tempRoute[i].action)=='on_road')then -tempForm=onRoadForm -else -tempForm=offRoadForm -end -if type(overRideSpeed)=='number'then -tempSpeed=overRideSpeed -end -useRoute[#useRoute+1]=routines.ground.buildWP(tempRoute[i],tempForm,tempSpeed) -end -if pType and string.lower(pType)=='doubleback'then -local curRoute=routines.utils.deepCopy(useRoute) -for i=#curRoute,2,-1 do -useRoute[#useRoute+1]=routines.ground.buildWP(curRoute[i],curRoute[i].action,curRoute[i].speed) -end -end -useRoute[1].action=useRoute[#useRoute].action -end -local cTask3={} -local newPatrol={} -newPatrol.route=useRoute -newPatrol.gpData=gpData:getName() -cTask3[#cTask3+1]='routines.ground.patrolRoute(' -cTask3[#cTask3+1]=routines.utils.oneLineSerialize(newPatrol) -cTask3[#cTask3+1]=')' -cTask3=table.concat(cTask3) -local tempTask={ -id='WrappedAction', -params={ -action={ -id='Script', -params={ -command=cTask3, -}, -}, -}, -} -useRoute[#useRoute].task=tempTask -routines.goRoute(gpData,useRoute) -return -end -routines.ground.patrol=function(gpData,pType,form,speed) -local vars={} -if type(gpData)=='table'and gpData:getName()then -gpData=gpData:getName() -end -vars.useGroupRoute=gpData -vars.gpData=gpData -vars.pType=pType -vars.offRoadForm=form -vars.speed=speed -routines.ground.patrolRoute(vars) -return -end -function routines.GetUnitHeight(CheckUnit) -local UnitPoint=CheckUnit:getPoint() -local UnitPosition={x=UnitPoint.x,y=UnitPoint.z} -local UnitHeight=UnitPoint.y -local LandHeight=land.getHeight(UnitPosition) -return UnitHeight-LandHeight -end -Su34Status={status={}} -boardMsgRed={statusMsg=""} -boardMsgAll={timeMsg=""} -SpawnSettings={} -Su34MenuPath={} -Su34Menus=0 -function Su34AttackCarlVinson(groupName) -local groupSu34=Group.getByName(groupName) -local controllerSu34=groupSu34.getController(groupSu34) -local groupCarlVinson=Group.getByName("US Carl Vinson #001") -controllerSu34.setOption(controllerSu34,AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.OPEN_FIRE) -controllerSu34.setOption(controllerSu34,AI.Option.Air.id.REACTION_ON_THREAT,AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE) -if groupCarlVinson~=nil then -controllerSu34.pushTask(controllerSu34,{id='AttackGroup',params={groupId=groupCarlVinson:getID(),expend=AI.Task.WeaponExpend.ALL,attackQtyLimit=true}}) -end -Su34Status.status[groupName]=1 -MessageToRed(string.format('%s: ',groupName)..'Attacking carrier Carl Vinson. ',10,'RedStatus'..groupName) -end -function Su34AttackWest(groupName) -local groupSu34=Group.getByName(groupName) -local controllerSu34=groupSu34.getController(groupSu34) -local groupShipWest1=Group.getByName("US Ship West #001") -local groupShipWest2=Group.getByName("US Ship West #002") -controllerSu34.setOption(controllerSu34,AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.OPEN_FIRE) -controllerSu34.setOption(controllerSu34,AI.Option.Air.id.REACTION_ON_THREAT,AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE) -if groupShipWest1~=nil then -controllerSu34.pushTask(controllerSu34,{id='AttackGroup',params={groupId=groupShipWest1:getID(),expend=AI.Task.WeaponExpend.ALL,attackQtyLimit=true}}) -end -if groupShipWest2~=nil then -controllerSu34.pushTask(controllerSu34,{id='AttackGroup',params={groupId=groupShipWest2:getID(),expend=AI.Task.WeaponExpend.ALL,attackQtyLimit=true}}) -end -Su34Status.status[groupName]=2 -MessageToRed(string.format('%s: ',groupName)..'Attacking invading ships in the west. ',10,'RedStatus'..groupName) -end -function Su34AttackNorth(groupName) -local groupSu34=Group.getByName(groupName) -local controllerSu34=groupSu34.getController(groupSu34) -local groupShipNorth1=Group.getByName("US Ship North #001") -local groupShipNorth2=Group.getByName("US Ship North #002") -local groupShipNorth3=Group.getByName("US Ship North #003") -controllerSu34.setOption(controllerSu34,AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.OPEN_FIRE) -controllerSu34.setOption(controllerSu34,AI.Option.Air.id.REACTION_ON_THREAT,AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE) -if groupShipNorth1~=nil then -controllerSu34.pushTask(controllerSu34,{id='AttackGroup',params={groupId=groupShipNorth1:getID(),expend=AI.Task.WeaponExpend.ALL,attackQtyLimit=false}}) -end -if groupShipNorth2~=nil then -controllerSu34.pushTask(controllerSu34,{id='AttackGroup',params={groupId=groupShipNorth2:getID(),expend=AI.Task.WeaponExpend.ALL,attackQtyLimit=false}}) -end -if groupShipNorth3~=nil then -controllerSu34.pushTask(controllerSu34,{id='AttackGroup',params={groupId=groupShipNorth3:getID(),expend=AI.Task.WeaponExpend.ALL,attackQtyLimit=false}}) -end -Su34Status.status[groupName]=3 -MessageToRed(string.format('%s: ',groupName)..'Attacking invading ships in the north. ',10,'RedStatus'..groupName) -end -function Su34Orbit(groupName) -local groupSu34=Group.getByName(groupName) -local controllerSu34=groupSu34:getController() -controllerSu34.setOption(controllerSu34,AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.WEAPON_HOLD) -controllerSu34.setOption(controllerSu34,AI.Option.Air.id.REACTION_ON_THREAT,AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE) -controllerSu34:pushTask({id='ControlledTask',params={task={id='Orbit',params={pattern=AI.Task.OrbitPattern.RACE_TRACK}},stopCondition={duration=600}}}) -Su34Status.status[groupName]=4 -MessageToRed(string.format('%s: ',groupName)..'In orbit and awaiting further instructions. ',10,'RedStatus'..groupName) -end -function Su34TakeOff(groupName) -local groupSu34=Group.getByName(groupName) -local controllerSu34=groupSu34:getController() -controllerSu34.setOption(controllerSu34,AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.WEAPON_HOLD) -controllerSu34.setOption(controllerSu34,AI.Option.Air.id.REACTION_ON_THREAT,AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE) -Su34Status.status[groupName]=8 -MessageToRed(string.format('%s: ',groupName)..'Take-Off. ',10,'RedStatus'..groupName) -end -function Su34Hold(groupName) -local groupSu34=Group.getByName(groupName) -local controllerSu34=groupSu34:getController() -controllerSu34.setOption(controllerSu34,AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.WEAPON_HOLD) -controllerSu34.setOption(controllerSu34,AI.Option.Air.id.REACTION_ON_THREAT,AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE) -Su34Status.status[groupName]=5 -MessageToRed(string.format('%s: ',groupName)..'Holding Weapons. ',10,'RedStatus'..groupName) -end -function Su34RTB(groupName) -Su34Status.status[groupName]=6 -MessageToRed(string.format('%s: ',groupName)..'Return to Krasnodar. ',10,'RedStatus'..groupName) -end -function Su34Destroyed(groupName) -Su34Status.status[groupName]=7 -MessageToRed(string.format('%s: ',groupName)..'Destroyed. ',30,'RedStatus'..groupName) -end -function GroupAlive(groupName) -local groupTest=Group.getByName(groupName) -local groupExists=false -if groupTest then -groupExists=groupTest:isExist() -end -return groupExists -end -function Su34IsDead() -end -function Su34OverviewStatus() -local msg="" -local currentStatus=0 -local Exists=false -for groupName,currentStatus in pairs(Su34Status.status)do -env.info(('Su34 Overview Status: GroupName = '..groupName)) -Alive=GroupAlive(groupName) -if Alive then -if currentStatus==1 then -msg=msg..string.format("%s: ",groupName) -msg=msg.."Attacking carrier Carl Vinson. " -elseif currentStatus==2 then -msg=msg..string.format("%s: ",groupName) -msg=msg.."Attacking supporting ships in the west. " -elseif currentStatus==3 then -msg=msg..string.format("%s: ",groupName) -msg=msg.."Attacking invading ships in the north. " -elseif currentStatus==4 then -msg=msg..string.format("%s: ",groupName) -msg=msg.."In orbit and awaiting further instructions. " -elseif currentStatus==5 then -msg=msg..string.format("%s: ",groupName) -msg=msg.."Holding Weapons. " -elseif currentStatus==6 then -msg=msg..string.format("%s: ",groupName) -msg=msg.."Return to Krasnodar. " -elseif currentStatus==7 then -msg=msg..string.format("%s: ",groupName) -msg=msg.."Destroyed. " -elseif currentStatus==8 then -msg=msg..string.format("%s: ",groupName) -msg=msg.."Take-Off. " -end -else -if currentStatus==7 then -msg=msg..string.format("%s: ",groupName) -msg=msg.."Destroyed. " -else -Su34Destroyed(groupName) -end -end -end -boardMsgRed.statusMsg=msg -end -function UpdateBoardMsg() -Su34OverviewStatus() -MessageToRed(boardMsgRed.statusMsg,15,'RedStatus') -end -function MusicReset(flg) -trigger.action.setUserFlag(95,flg) -end -function PlaneActivate(groupNameFormat,flg) -local groupName=groupNameFormat..string.format("#%03d",trigger.misc.getUserFlag(flg)) -trigger.action.activateGroup(Group.getByName(groupName)) -end -function Su34Menu(groupName) -local groupSu34=Group.getByName(groupName) -if Su34Status.status[groupName]==1 or -Su34Status.status[groupName]==2 or -Su34Status.status[groupName]==3 or -Su34Status.status[groupName]==4 or -Su34Status.status[groupName]==5 then -if Su34MenuPath[groupName]==nil then -if planeMenuPath==nil then -planeMenuPath=missionCommands.addSubMenuForCoalition( -coalition.side.RED, -"SU-34 anti-ship flights", -nil -) -end -Su34MenuPath[groupName]=missionCommands.addSubMenuForCoalition( -coalition.side.RED, -"Flight "..groupName, -planeMenuPath -) -missionCommands.addCommandForCoalition( -coalition.side.RED, -"Attack carrier Carl Vinson", -Su34MenuPath[groupName], -Su34AttackCarlVinson, -groupName -) -missionCommands.addCommandForCoalition( -coalition.side.RED, -"Attack ships in the west", -Su34MenuPath[groupName], -Su34AttackWest, -groupName -) -missionCommands.addCommandForCoalition( -coalition.side.RED, -"Attack ships in the north", -Su34MenuPath[groupName], -Su34AttackNorth, -groupName -) -missionCommands.addCommandForCoalition( -coalition.side.RED, -"Hold position and await instructions", -Su34MenuPath[groupName], -Su34Orbit, -groupName -) -missionCommands.addCommandForCoalition( -coalition.side.RED, -"Report status", -Su34MenuPath[groupName], -Su34OverviewStatus -) -end -else -if Su34MenuPath[groupName]then -missionCommands.removeItemForCoalition(coalition.side.RED,Su34MenuPath[groupName]) -end -end -end -function ChooseInfantry(TeleportPrefixTable,TeleportMax) -TeleportPrefixTableCount=#TeleportPrefixTable -TeleportPrefixTableIndex=math.random(1,TeleportPrefixTableCount) -local TeleportFound=false -local TeleportLoop=true -local Index=TeleportPrefixTableIndex -local TeleportPrefix='' -while TeleportLoop do -TeleportPrefix=TeleportPrefixTable[Index] -if SpawnSettings[TeleportPrefix]then -if SpawnSettings[TeleportPrefix]['SpawnCount']-10 then -local PlayerFound=false -local MusicStart=0 -local MusicTime=0 -for SndQueueIdx,SndQueue in pairs(_MusicTable.Queue)do -if SndQueue.PlayerName==PlayerName then -PlayerFound=true -MusicStart=SndQueue.Start -MusicTime=_MusicTable.Files[SndQueue.Ref].Time -break -end -end -if PlayerFound then -if MusicStart+MusicTime<=timer.getTime()then -MusicOut=true -end -else -MusicOut=true -end -end -if MusicOut then -else -end -return MusicOut -end -function MusicScheduler() -if _MusicTable['Queue']~=nil and _MusicTable.FileCnt>0 then -for SndQueueIdx,SndQueue in pairs(_MusicTable.Queue)do -if SndQueue.Continue then -if MusicCanStart(SndQueue.PlayerName)then -MusicToPlayer('',SndQueue.PlayerName,true) -end -end -end -end -end -env.info(('Init: Scripts Loaded v1.1')) -SMOKECOLOR=trigger.smokeColor -FLARECOLOR=trigger.flareColor -UTILS={ -_MarkID=1 -} -UTILS.IsInstanceOf=function(object,className) -if not type(className)=='string'then -if type(className)=='table'and className.IsInstanceOf~=nil then -className=className.ClassName -else -local err_str='className parameter should be a string; parameter received: '..type(className) -self:E(err_str) -return false -end -end -if type(object)=='table'and object.IsInstanceOf~=nil then -return object:IsInstanceOf(className) -else -local basicDataTypes={'string','number','function','boolean','nil','table'} -for _,basicDataType in ipairs(basicDataTypes)do -if className==basicDataType then -return type(object)==basicDataType -end -end -end -return false -end -UTILS.DeepCopy=function(object) -local lookup_table={} -local function _copy(object) -if type(object)~="table"then -return object -elseif lookup_table[object]then -return lookup_table[object] -end -local new_table={} -lookup_table[object]=new_table -for index,value in pairs(object)do -new_table[_copy(index)]=_copy(value) -end -return setmetatable(new_table,getmetatable(object)) -end -local objectreturn=_copy(object) -return objectreturn -end -UTILS.OneLineSerialize=function(tbl) -lookup_table={} -local function _Serialize(tbl) -if type(tbl)=='table'then -if lookup_table[tbl]then -return lookup_table[object] -end -local tbl_str={} -lookup_table[tbl]=tbl_str -tbl_str[#tbl_str+1]='{' -for ind,val in pairs(tbl)do -local ind_str={} -if type(ind)=="number"then -ind_str[#ind_str+1]='[' -ind_str[#ind_str+1]=tostring(ind) -ind_str[#ind_str+1]=']=' -else -ind_str[#ind_str+1]='[' -ind_str[#ind_str+1]=routines.utils.basicSerialize(ind) -ind_str[#ind_str+1]=']=' -end -local val_str={} -if((type(val)=='number')or(type(val)=='boolean'))then -val_str[#val_str+1]=tostring(val) -val_str[#val_str+1]=',' -tbl_str[#tbl_str+1]=table.concat(ind_str) -tbl_str[#tbl_str+1]=table.concat(val_str) -elseif type(val)=='string'then -val_str[#val_str+1]=routines.utils.basicSerialize(val) -val_str[#val_str+1]=',' -tbl_str[#tbl_str+1]=table.concat(ind_str) -tbl_str[#tbl_str+1]=table.concat(val_str) -elseif type(val)=='nil'then -val_str[#val_str+1]='nil,' -tbl_str[#tbl_str+1]=table.concat(ind_str) -tbl_str[#tbl_str+1]=table.concat(val_str) -elseif type(val)=='table'then -if ind=="__index"then -else -val_str[#val_str+1]=_Serialize(val) -val_str[#val_str+1]=',' -tbl_str[#tbl_str+1]=table.concat(ind_str) -tbl_str[#tbl_str+1]=table.concat(val_str) -end -elseif type(val)=='function'then -tbl_str[#tbl_str+1]="f() "..tostring(ind) -tbl_str[#tbl_str+1]=',' -else -env.info('unable to serialize value type '..routines.utils.basicSerialize(type(val))..' at index '..tostring(ind)) -env.info(debug.traceback()) -end -end -tbl_str[#tbl_str+1]='}' -return table.concat(tbl_str) -else -return tostring(tbl) -end -end -local objectreturn=_Serialize(tbl) -return objectreturn -end -UTILS.BasicSerialize=function(s) -if s==nil then -return"\"\"" -else -if((type(s)=='number')or(type(s)=='boolean')or(type(s)=='function')or(type(s)=='table')or(type(s)=='userdata'))then -return tostring(s) -elseif type(s)=='string'then -s=string.format('%q',s) -return s -end -end -end -UTILS.ToDegree=function(angle) -return angle*180/math.pi -end -UTILS.ToRadian=function(angle) -return angle*math.pi/180 -end -UTILS.MetersToNM=function(meters) -return meters/1852 -end -UTILS.MetersToFeet=function(meters) -return meters/0.3048 -end -UTILS.NMToMeters=function(NM) -return NM*1852 -end -UTILS.FeetToMeters=function(feet) -return feet*0.3048 -end -UTILS.MpsToKnots=function(mps) -return mps*3600/1852 -end -UTILS.MpsToKmph=function(mps) -return mps*3.6 -end -UTILS.KnotsToMps=function(knots) -return knots*1852/3600 -end -UTILS.KnotsToKmph=function(knots) -return knots*1.852 -end -UTILS.KmphToMps=function(kmph) -return kmph/3.6 -end -UTILS.tostringLL=function(lat,lon,acc,DMS) -local latHemi,lonHemi -if lat>0 then -latHemi='N' -else -latHemi='S' -end -if lon>0 then -lonHemi='E' -else -lonHemi='W' -end -lat=math.abs(lat) -lon=math.abs(lon) -local latDeg=math.floor(lat) -local latMin=(lat-latDeg)*60 -local lonDeg=math.floor(lon) -local lonMin=(lon-lonDeg)*60 -if DMS then -local oldLatMin=latMin -latMin=math.floor(latMin) -local latSec=UTILS.Round((oldLatMin-latMin)*60,acc) -local oldLonMin=lonMin -lonMin=math.floor(lonMin) -local lonSec=UTILS.Round((oldLonMin-lonMin)*60,acc) -if latSec==60 then -latSec=0 -latMin=latMin+1 -end -if lonSec==60 then -lonSec=0 -lonMin=lonMin+1 -end -local secFrmtStr -secFrmtStr='%02d' -return string.format('%02d',latDeg)..' '..string.format('%02d',latMin)..'\' '..string.format(secFrmtStr,latSec)..'"'..latHemi..' ' -..string.format('%02d',lonDeg)..' '..string.format('%02d',lonMin)..'\' '..string.format(secFrmtStr,lonSec)..'"'..lonHemi -else -latMin=UTILS.Round(latMin,acc) -lonMin=UTILS.Round(lonMin,acc) -if latMin==60 then -latMin=0 -latDeg=latDeg+1 -end -if lonMin==60 then -lonMin=0 -lonDeg=lonDeg+1 -end -local minFrmtStr -if acc<=0 then -minFrmtStr='%02d' -else -local width=3+acc -minFrmtStr='%0'..width..'.'..acc..'f' -end -return string.format('%02d',latDeg)..' '..string.format(minFrmtStr,latMin)..'\''..latHemi..' ' -..string.format('%02d',lonDeg)..' '..string.format(minFrmtStr,lonMin)..'\''..lonHemi -end -end -UTILS.tostringMGRS=function(MGRS,acc) -if acc==0 then -return MGRS.UTMZone..' '..MGRS.MGRSDigraph -else -return MGRS.UTMZone..' '..MGRS.MGRSDigraph..' '..string.format('%0'..acc..'d',UTILS.Round(MGRS.Easting/(10^(5-acc)),0)) -..' '..string.format('%0'..acc..'d',UTILS.Round(MGRS.Northing/(10^(5-acc)),0)) -end -end -function UTILS.Round(num,idp) -local mult=10^(idp or 0) -return math.floor(num*mult+0.5)/mult -end -function UTILS.DoString(s) -local f,err=loadstring(s) -if f then -return true,f() -else -return false,err -end -end -function UTILS.spairs(t,order) -local keys={} -for k in pairs(t)do keys[#keys+1]=k end -if order then -table.sort(keys,function(a,b)return order(t,a,b)end) -else -table.sort(keys) -end -local i=0 -return function() -i=i+1 -if keys[i]then -return keys[i],t[keys[i]] -end -end -end -function UTILS.GetMarkID() -UTILS._MarkID=UTILS._MarkID+1 -return UTILS._MarkID -end -function UTILS.IsInRadius(InVec2,Vec2,Radius) -local InRadius=((InVec2.x-Vec2.x)^2+(InVec2.y-Vec2.y)^2)^0.5<=Radius -return InRadius -end -function UTILS.IsInSphere(InVec3,Vec3,Radius) -local InSphere=((InVec3.x-Vec3.x)^2+(InVec3.y-Vec3.y)^2+(InVec3.z-Vec3.z)^2)^0.5<=Radius -return InSphere -end -local _TraceOnOff=true -local _TraceLevel=1 -local _TraceAll=false -local _TraceClass={} -local _TraceClassMethod={} -local _ClassID=0 -BASE={ -ClassName="BASE", -ClassID=0, -Events={}, -States={}, -} -BASE.__={} -BASE._={ -Schedules={} -} -FORMATION={ -Cone="Cone", -Vee="Vee" -} -function BASE:New() -local self=routines.utils.deepCopy(self) -_ClassID=_ClassID+1 -self.ClassID=_ClassID -return self -end -function BASE:Inherit(Child,Parent) -local Child=routines.utils.deepCopy(Child) -if Child~=nil then -if rawget(Child,"__")then -setmetatable(Child,{__index=Child.__}) -setmetatable(Child.__,{__index=Parent}) -else -setmetatable(Child,{__index=Parent}) -end -end -return Child -end -function BASE:GetParent(Child) -local Parent -if Child.ClassName=='BASE'then -Parent=nil -elseif rawget(Child,"__")then -Parent=getmetatable(Child.__).__index -else -Parent=getmetatable(Child).__index -end -return Parent -end -function BASE:IsInstanceOf(ClassName) -if type(ClassName)~='string'then -if type(ClassName)=='table'and ClassName.ClassName~=nil then -ClassName=ClassName.ClassName -else -local err_str='className parameter should be a string; parameter received: '..type(ClassName) -self:E(err_str) -return false -end -end -ClassName=string.upper(ClassName) -if string.upper(self.ClassName)==ClassName then -return true -end -local Parent=self:GetParent(self) -while Parent do -if string.upper(Parent.ClassName)==ClassName then -return true -end -Parent=Parent:GetParent(Parent) -end -return false -end -function BASE:GetClassNameAndID() -return string.format('%s#%09d',self.ClassName,self.ClassID) -end -function BASE:GetClassName() -return self.ClassName -end -function BASE:GetClassID() -return self.ClassID -end -do -function BASE:EventDispatcher() -return _EVENTDISPATCHER -end -function BASE:GetEventPriority() -return self._.EventPriority or 5 -end -function BASE:SetEventPriority(EventPriority) -self._.EventPriority=EventPriority -end -function BASE:EventRemoveAll() -self:EventDispatcher():RemoveAll(self) -return self -end -function BASE:HandleEvent(Event,EventFunction) -self:EventDispatcher():OnEventGeneric(EventFunction,self,Event) -return self -end -function BASE:UnHandleEvent(Event) -self:EventDispatcher():RemoveEvent(self,Event) -return self -end -end -function BASE:CreateEventBirth(EventTime,Initiator,IniUnitName,place,subplace) -self:F({EventTime,Initiator,IniUnitName,place,subplace}) -local Event={ -id=world.event.S_EVENT_BIRTH, -time=EventTime, -initiator=Initiator, -IniUnitName=IniUnitName, -place=place, -subplace=subplace -} -world.onEvent(Event) -end -function BASE:CreateEventCrash(EventTime,Initiator) -self:F({EventTime,Initiator}) -local Event={ -id=world.event.S_EVENT_CRASH, -time=EventTime, -initiator=Initiator, -} -world.onEvent(Event) -end -function BASE:CreateEventTakeoff(EventTime,Initiator) -self:F({EventTime,Initiator}) -local Event={ -id=world.event.S_EVENT_TAKEOFF, -time=EventTime, -initiator=Initiator, -} -world.onEvent(Event) -end -function BASE:onEvent(event) -if self then -for EventID,EventObject in pairs(self.Events)do -if EventObject.EventEnabled then -if event.id==EventObject.Event then -if self==EventObject.Self then -if event.initiator and event.initiator:isExist()then -event.IniUnitName=event.initiator:getName() -end -if event.target and event.target:isExist()then -event.TgtUnitName=event.target:getName() -end -end -end -end -end -end -end -do -function BASE:ScheduleOnce(Start,SchedulerFunction,...) -self:F2({Start}) -self:T3({...}) -local ObjectName="-" -ObjectName=self.ClassName..self.ClassID -self:F3({"ScheduleOnce: ",ObjectName,Start}) -self.SchedulerObject=self -local ScheduleID=_SCHEDULEDISPATCHER:AddSchedule( -self, -SchedulerFunction, -{...}, -Start, -nil, -nil, -nil -) -self._.Schedules[#self.Schedules+1]=ScheduleID -return self._.Schedules -end -function BASE:ScheduleRepeat(Start,Repeat,RandomizeFactor,Stop,SchedulerFunction,...) -self:F2({Start}) -self:T3({...}) -local ObjectName="-" -ObjectName=self.ClassName..self.ClassID -self:F3({"ScheduleRepeat: ",ObjectName,Start,Repeat,RandomizeFactor,Stop}) -self.SchedulerObject=self -local ScheduleID=_SCHEDULEDISPATCHER:AddSchedule( -self, -SchedulerFunction, -{...}, -Start, -Repeat, -RandomizeFactor, -Stop -) -self._.Schedules[SchedulerFunction]=ScheduleID -return self._.Schedules -end -function BASE:ScheduleStop(SchedulerFunction) -self:F3({"ScheduleStop:"}) -_SCHEDULEDISPATCHER:Stop(self,self._.Schedules[SchedulerFunction]) -end -end -function BASE:SetState(Object,Key,Value) -local ClassNameAndID=Object:GetClassNameAndID() -self.States[ClassNameAndID]=self.States[ClassNameAndID]or{} -self.States[ClassNameAndID][Key]=Value -return self.States[ClassNameAndID][Key] -end -function BASE:GetState(Object,Key) -local ClassNameAndID=Object:GetClassNameAndID() -if self.States[ClassNameAndID]then -local Value=self.States[ClassNameAndID][Key]or false -return Value -end -return nil -end -function BASE:ClearState(Object,StateName) -local ClassNameAndID=Object:GetClassNameAndID() -if self.States[ClassNameAndID]then -self.States[ClassNameAndID][StateName]=nil -end -end -function BASE:TraceOnOff(TraceOnOff) -_TraceOnOff=TraceOnOff -end -function BASE:IsTrace() -if debug and(_TraceAll==true)or(_TraceClass[self.ClassName]or _TraceClassMethod[self.ClassName])then -return true -else -return false -end -end -function BASE:TraceLevel(Level) -_TraceLevel=Level -self:E("Tracing level "..Level) -end -function BASE:TraceAll(TraceAll) -_TraceAll=TraceAll -if _TraceAll then -self:E("Tracing all methods in MOOSE ") -else -self:E("Switched off tracing all methods in MOOSE") -end -end -function BASE:TraceClass(Class) -_TraceClass[Class]=true -_TraceClassMethod[Class]={} -self:E("Tracing class "..Class) -end -function BASE:TraceClassMethod(Class,Method) -if not _TraceClassMethod[Class]then -_TraceClassMethod[Class]={} -_TraceClassMethod[Class].Method={} -end -_TraceClassMethod[Class].Method[Method]=true -self:E("Tracing method "..Method.." of class "..Class) -end -function BASE:_F(Arguments,DebugInfoCurrentParam,DebugInfoFromParam) -if debug and(_TraceAll==true)or(_TraceClass[self.ClassName]or _TraceClassMethod[self.ClassName])then -local DebugInfoCurrent=DebugInfoCurrentParam and DebugInfoCurrentParam or debug.getinfo(2,"nl") -local DebugInfoFrom=DebugInfoFromParam and DebugInfoFromParam or debug.getinfo(3,"l") -local Function="function" -if DebugInfoCurrent.name then -Function=DebugInfoCurrent.name -end -if _TraceAll==true or _TraceClass[self.ClassName]or _TraceClassMethod[self.ClassName].Method[Function]then -local LineCurrent=0 -if DebugInfoCurrent.currentline then -LineCurrent=DebugInfoCurrent.currentline -end -local LineFrom=0 -if DebugInfoFrom then -LineFrom=DebugInfoFrom.currentline -end -env.info(string.format("%6d(%6d)/%1s:%20s%05d.%s(%s)",LineCurrent,LineFrom,"F",self.ClassName,self.ClassID,Function,routines.utils.oneLineSerialize(Arguments))) -end -end -end -function BASE:F(Arguments) -if debug and _TraceOnOff then -local DebugInfoCurrent=debug.getinfo(2,"nl") -local DebugInfoFrom=debug.getinfo(3,"l") -if _TraceLevel>=1 then -self:_F(Arguments,DebugInfoCurrent,DebugInfoFrom) -end -end -end -function BASE:F2(Arguments) -if debug and _TraceOnOff then -local DebugInfoCurrent=debug.getinfo(2,"nl") -local DebugInfoFrom=debug.getinfo(3,"l") -if _TraceLevel>=2 then -self:_F(Arguments,DebugInfoCurrent,DebugInfoFrom) -end -end -end -function BASE:F3(Arguments) -if debug and _TraceOnOff then -local DebugInfoCurrent=debug.getinfo(2,"nl") -local DebugInfoFrom=debug.getinfo(3,"l") -if _TraceLevel>=3 then -self:_F(Arguments,DebugInfoCurrent,DebugInfoFrom) -end -end -end -function BASE:_T(Arguments,DebugInfoCurrentParam,DebugInfoFromParam) -if debug and(_TraceAll==true)or(_TraceClass[self.ClassName]or _TraceClassMethod[self.ClassName])then -local DebugInfoCurrent=DebugInfoCurrentParam and DebugInfoCurrentParam or debug.getinfo(2,"nl") -local DebugInfoFrom=DebugInfoFromParam and DebugInfoFromParam or debug.getinfo(3,"l") -local Function="function" -if DebugInfoCurrent.name then -Function=DebugInfoCurrent.name -end -if _TraceAll==true or _TraceClass[self.ClassName]or _TraceClassMethod[self.ClassName].Method[Function]then -local LineCurrent=0 -if DebugInfoCurrent.currentline then -LineCurrent=DebugInfoCurrent.currentline -end -local LineFrom=0 -if DebugInfoFrom then -LineFrom=DebugInfoFrom.currentline -end -env.info(string.format("%6d(%6d)/%1s:%20s%05d.%s",LineCurrent,LineFrom,"T",self.ClassName,self.ClassID,routines.utils.oneLineSerialize(Arguments))) -end -end -end -function BASE:T(Arguments) -if debug and _TraceOnOff then -local DebugInfoCurrent=debug.getinfo(2,"nl") -local DebugInfoFrom=debug.getinfo(3,"l") -if _TraceLevel>=1 then -self:_T(Arguments,DebugInfoCurrent,DebugInfoFrom) -end -end -end -function BASE:T2(Arguments) -if debug and _TraceOnOff then -local DebugInfoCurrent=debug.getinfo(2,"nl") -local DebugInfoFrom=debug.getinfo(3,"l") -if _TraceLevel>=2 then -self:_T(Arguments,DebugInfoCurrent,DebugInfoFrom) -end -end -end -function BASE:T3(Arguments) -if debug and _TraceOnOff then -local DebugInfoCurrent=debug.getinfo(2,"nl") -local DebugInfoFrom=debug.getinfo(3,"l") -if _TraceLevel>=3 then -self:_T(Arguments,DebugInfoCurrent,DebugInfoFrom) -end -end -end -function BASE:E(Arguments) -if debug then -local DebugInfoCurrent=debug.getinfo(2,"nl") -local DebugInfoFrom=debug.getinfo(3,"l") -local Function="function" -if DebugInfoCurrent.name then -Function=DebugInfoCurrent.name -end -local LineCurrent=DebugInfoCurrent.currentline -local LineFrom=-1 -if DebugInfoFrom then -LineFrom=DebugInfoFrom.currentline -end -env.info(string.format("%6d(%6d)/%1s:%20s%05d.%s(%s)",LineCurrent,LineFrom,"E",self.ClassName,self.ClassID,Function,routines.utils.oneLineSerialize(Arguments))) -end -end -REPORT={ -ClassName="REPORT", -Title="", -} -function REPORT:New(Title) -local self=BASE:Inherit(self,BASE:New()) -self.Report={} -self:SetTitle(Title or"") -self:SetIndent(3) -return self -end -function REPORT:HasText() -return#self.Report>0 -end -function REPORT:SetIndent(Indent) -self.Indent=Indent -return self -end -function REPORT:Add(Text) -self.Report[#self.Report+1]=Text -return self -end -function REPORT:AddIndent(Text) -self.Report[#self.Report+1]=string.rep(" ",self.Indent)..Text:gsub("\n","\n"..string.rep(" ",self.Indent)) -return self -end -function REPORT:Text(Delimiter) -Delimiter=Delimiter or"\n" -local ReportText=(self.Title~=""and self.Title..Delimiter or self.Title)..table.concat(self.Report,Delimiter)or"" -return ReportText -end -function REPORT:SetTitle(Title) -self.Title=Title -return self -end -function REPORT:GetCount() -return#self.Report -end -SCHEDULER={ -ClassName="SCHEDULER", -Schedules={}, -} -function SCHEDULER:New(SchedulerObject,SchedulerFunction,SchedulerArguments,Start,Repeat,RandomizeFactor,Stop) -local self=BASE:Inherit(self,BASE:New()) -self:F2({Start,Repeat,RandomizeFactor,Stop}) -local ScheduleID=nil -self.MasterObject=SchedulerObject -if SchedulerFunction then -ScheduleID=self:Schedule(SchedulerObject,SchedulerFunction,SchedulerArguments,Start,Repeat,RandomizeFactor,Stop) -end -return self,ScheduleID -end -function SCHEDULER:Schedule(SchedulerObject,SchedulerFunction,SchedulerArguments,Start,Repeat,RandomizeFactor,Stop) -self:F2({Start,Repeat,RandomizeFactor,Stop}) -self:T3({SchedulerArguments}) -local ObjectName="-" -if SchedulerObject and SchedulerObject.ClassName and SchedulerObject.ClassID then -ObjectName=SchedulerObject.ClassName..SchedulerObject.ClassID -end -self:F3({"Schedule :",ObjectName,tostring(SchedulerObject),Start,Repeat,RandomizeFactor,Stop}) -self.SchedulerObject=SchedulerObject -local ScheduleID=_SCHEDULEDISPATCHER:AddSchedule( -self, -SchedulerFunction, -SchedulerArguments, -Start, -Repeat, -RandomizeFactor, -Stop -) -self.Schedules[#self.Schedules+1]=ScheduleID -return ScheduleID -end -function SCHEDULER:Start(ScheduleID) -self:F3({ScheduleID}) -_SCHEDULEDISPATCHER:Start(self,ScheduleID) -end -function SCHEDULER:Stop(ScheduleID) -self:F3({ScheduleID}) -_SCHEDULEDISPATCHER:Stop(self,ScheduleID) -end -function SCHEDULER:Remove(ScheduleID) -self:F3({ScheduleID}) -_SCHEDULEDISPATCHER:Remove(self,ScheduleID) -end -function SCHEDULER:Clear() -self:F3() -_SCHEDULEDISPATCHER:Clear(self) -end -SCHEDULEDISPATCHER={ -ClassName="SCHEDULEDISPATCHER", -CallID=0, -} -function SCHEDULEDISPATCHER:New() -local self=BASE:Inherit(self,BASE:New()) -self:F3() -return self -end -function SCHEDULEDISPATCHER:AddSchedule(Scheduler,ScheduleFunction,ScheduleArguments,Start,Repeat,Randomize,Stop) -self:F2({Scheduler,ScheduleFunction,ScheduleArguments,Start,Repeat,Randomize,Stop}) -self.CallID=self.CallID+1 -local CallID=self.CallID.."#"..(Scheduler.MasterObject and Scheduler.MasterObject.GetClassNameAndID and Scheduler.MasterObject:GetClassNameAndID()or"")or"" -self.PersistentSchedulers=self.PersistentSchedulers or{} -self.ObjectSchedulers=self.ObjectSchedulers or setmetatable({},{__mode="v"}) -if Scheduler.MasterObject then -self.ObjectSchedulers[CallID]=Scheduler -self:F3({CallID=CallID,ObjectScheduler=tostring(self.ObjectSchedulers[CallID]),MasterObject=tostring(Scheduler.MasterObject)}) -else -self.PersistentSchedulers[CallID]=Scheduler -self:F3({CallID=CallID,PersistentScheduler=self.PersistentSchedulers[CallID]}) -end -self.Schedule=self.Schedule or setmetatable({},{__mode="k"}) -self.Schedule[Scheduler]=self.Schedule[Scheduler]or{} -self.Schedule[Scheduler][CallID]={} -self.Schedule[Scheduler][CallID].Function=ScheduleFunction -self.Schedule[Scheduler][CallID].Arguments=ScheduleArguments -self.Schedule[Scheduler][CallID].StartTime=timer.getTime()+(Start or 0) -self.Schedule[Scheduler][CallID].Start=Start+.1 -self.Schedule[Scheduler][CallID].Repeat=Repeat or 0 -self.Schedule[Scheduler][CallID].Randomize=Randomize or 0 -self.Schedule[Scheduler][CallID].Stop=Stop -self:T3(self.Schedule[Scheduler][CallID]) -self.Schedule[Scheduler][CallID].CallHandler=function(CallID) -self:F2(CallID) -local ErrorHandler=function(errmsg) -env.info("Error in timer function: "..errmsg) -if debug~=nil then -env.info(debug.traceback()) -end -return errmsg -end -local Scheduler=self.ObjectSchedulers[CallID] -if not Scheduler then -Scheduler=self.PersistentSchedulers[CallID] -end -if Scheduler then -local MasterObject=tostring(Scheduler.MasterObject) -local Schedule=self.Schedule[Scheduler][CallID] -local ScheduleObject=Scheduler.SchedulerObject -local ScheduleFunction=Schedule.Function -local ScheduleArguments=Schedule.Arguments -local Start=Schedule.Start -local Repeat=Schedule.Repeat or 0 -local Randomize=Schedule.Randomize or 0 -local Stop=Schedule.Stop or 0 -local ScheduleID=Schedule.ScheduleID -local Status,Result -if ScheduleObject then -local function Timer() -return ScheduleFunction(ScheduleObject,unpack(ScheduleArguments)) -end -Status,Result=xpcall(Timer,ErrorHandler) -else -local function Timer() -return ScheduleFunction(unpack(ScheduleArguments)) -end -Status,Result=xpcall(Timer,ErrorHandler) -end -local CurrentTime=timer.getTime() -local StartTime=Schedule.StartTime -self:F3({Master=MasterObject,CurrentTime=CurrentTime,StartTime=StartTime,Start=Start,Repeat=Repeat,Randomize=Randomize,Stop=Stop}) -if Status and((Result==nil)or(Result and Result~=false))then -if Repeat~=0 and((Stop==0)or(Stop~=0 and CurrentTime<=StartTime+Stop))then -local ScheduleTime= -CurrentTime+ -Repeat+ -math.random( --(Randomize*Repeat/2), -(Randomize*Repeat/2) -)+ -0.01 -return ScheduleTime -else -self:Stop(Scheduler,CallID) -end -else -self:Stop(Scheduler,CallID) -end -else -self:E("Scheduled obsolete call for CallID: "..CallID) -end -return nil -end -self:Start(Scheduler,CallID) -return CallID -end -function SCHEDULEDISPATCHER:RemoveSchedule(Scheduler,CallID) -self:F2({Remove=CallID,Scheduler=Scheduler}) -if CallID then -self:Stop(Scheduler,CallID) -self.Schedule[Scheduler][CallID]=nil -end -end -function SCHEDULEDISPATCHER:Start(Scheduler,CallID) -self:F2({Start=CallID,Scheduler=Scheduler}) -if CallID then -local Schedule=self.Schedule[Scheduler] -if not Schedule[CallID].ScheduleID then -Schedule[CallID].StartTime=timer.getTime() -Schedule[CallID].ScheduleID=timer.scheduleFunction( -Schedule[CallID].CallHandler, -CallID, -timer.getTime()+Schedule[CallID].Start -) -end -else -for CallID,Schedule in pairs(self.Schedule[Scheduler]or{})do -self:Start(Scheduler,CallID) -end -end -end -function SCHEDULEDISPATCHER:Stop(Scheduler,CallID) -self:F2({Stop=CallID,Scheduler=Scheduler}) -if CallID then -local Schedule=self.Schedule[Scheduler] -if Schedule[CallID].ScheduleID then -timer.removeFunction(Schedule[CallID].ScheduleID) -Schedule[CallID].ScheduleID=nil -end -else -for CallID,Schedule in pairs(self.Schedule[Scheduler]or{})do -self:Stop(Scheduler,CallID) -end -end -end -function SCHEDULEDISPATCHER:Clear(Scheduler) -self:F2({Scheduler=Scheduler}) -for CallID,Schedule in pairs(self.Schedule[Scheduler]or{})do -self:Stop(Scheduler,CallID) -end -end -EVENT={ -ClassName="EVENT", -ClassID=0, -} -world.event.S_EVENT_NEW_CARGO=world.event.S_EVENT_MAX+1000 -world.event.S_EVENT_DELETE_CARGO=world.event.S_EVENT_MAX+1001 -EVENTS={ -Shot=world.event.S_EVENT_SHOT, -Hit=world.event.S_EVENT_HIT, -Takeoff=world.event.S_EVENT_TAKEOFF, -Land=world.event.S_EVENT_LAND, -Crash=world.event.S_EVENT_CRASH, -Ejection=world.event.S_EVENT_EJECTION, -Refueling=world.event.S_EVENT_REFUELING, -Dead=world.event.S_EVENT_DEAD, -PilotDead=world.event.S_EVENT_PILOT_DEAD, -BaseCaptured=world.event.S_EVENT_BASE_CAPTURED, -MissionStart=world.event.S_EVENT_MISSION_START, -MissionEnd=world.event.S_EVENT_MISSION_END, -TookControl=world.event.S_EVENT_TOOK_CONTROL, -RefuelingStop=world.event.S_EVENT_REFUELING_STOP, -Birth=world.event.S_EVENT_BIRTH, -HumanFailure=world.event.S_EVENT_HUMAN_FAILURE, -EngineStartup=world.event.S_EVENT_ENGINE_STARTUP, -EngineShutdown=world.event.S_EVENT_ENGINE_SHUTDOWN, -PlayerEnterUnit=world.event.S_EVENT_PLAYER_ENTER_UNIT, -PlayerLeaveUnit=world.event.S_EVENT_PLAYER_LEAVE_UNIT, -PlayerComment=world.event.S_EVENT_PLAYER_COMMENT, -ShootingStart=world.event.S_EVENT_SHOOTING_START, -ShootingEnd=world.event.S_EVENT_SHOOTING_END, -NewCargo=world.event.S_EVENT_NEW_CARGO, -DeleteCargo=world.event.S_EVENT_DELETE_CARGO, -} -local _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" -}, -[EVENTS.NewCargo]={ -Order=1, -Event="OnEventNewCargo", -Text="S_EVENT_NEW_CARGO" -}, -[EVENTS.DeleteCargo]={ -Order=1, -Event="OnEventDeleteCargo", -Text="S_EVENT_DELETE_CARGO" -}, -} -function EVENT:New() -local self=BASE:Inherit(self,BASE:New()) -self:F2() -self.EventHandler=world.addEventHandler(self) -return self -end -function EVENT:Init(EventID,EventClass) -self:F3({_EVENTMETA[EventID].Text,EventClass}) -if not self.Events[EventID]then -self.Events[EventID]={} -end -local EventPriority=EventClass:GetEventPriority() -if not self.Events[EventID][EventPriority]then -self.Events[EventID][EventPriority]=setmetatable({},{__mode="k"}) -end -if not self.Events[EventID][EventPriority][EventClass]then -self.Events[EventID][EventPriority][EventClass]={} -end -return self.Events[EventID][EventPriority][EventClass] -end -function EVENT:RemoveEvent(EventClass,EventID) -self:F2({"Removing subscription for class: ",EventClass:GetClassNameAndID()}) -local EventPriority=EventClass:GetEventPriority() -self.Events=self.Events or{} -self.Events[EventID]=self.Events[EventID]or{} -self.Events[EventID][EventPriority]=self.Events[EventID][EventPriority]or{} -self.Events[EventID][EventPriority][EventClass]=self.Events[EventID][EventPriority][EventClass] -self.Events[EventID][EventPriority][EventClass]=nil -end -function EVENT:Reset(EventObject) -self:E({"Resetting subscriptions for class: ",EventObject:GetClassNameAndID()}) -local EventPriority=EventObject:GetEventPriority() -for EventID,EventData in pairs(self.Events)do -if self.EventsDead then -if self.EventsDead[EventID]then -if self.EventsDead[EventID][EventPriority]then -if self.EventsDead[EventID][EventPriority][EventObject]then -self.Events[EventID][EventPriority][EventObject]=self.EventsDead[EventID][EventPriority][EventObject] -end -end -end -end -end -end -function EVENT:RemoveAll(EventObject) -self:F3({EventObject:GetClassNameAndID()}) -local EventClass=EventObject:GetClassNameAndID() -local EventPriority=EventClass:GetEventPriority() -for EventID,EventData in pairs(self.Events)do -self.Events[EventID][EventPriority][EventClass]=nil -end -end -function EVENT:OnEventForTemplate(EventTemplate,EventFunction,EventClass,EventID) -self:F2(EventTemplate.name) -for EventUnitID,EventUnit in pairs(EventTemplate.units)do -self:OnEventForUnit(EventUnit.name,EventFunction,EventClass,EventID) -end -return self -end -function EVENT:OnEventGeneric(EventFunction,EventClass,EventID) -self:F2({EventID}) -local EventData=self:Init(EventID,EventClass) -EventData.EventFunction=EventFunction -return self -end -function EVENT:OnEventForUnit(UnitName,EventFunction,EventClass,EventID) -self:F2(UnitName) -local EventData=self:Init(EventID,EventClass) -EventData.EventUnit=true -EventData.EventFunction=EventFunction -return self -end -function EVENT:OnEventForGroup(GroupName,EventFunction,EventClass,EventID,...) -self:E(GroupName) -local Event=self:Init(EventID,EventClass) -Event.EventGroup=true -Event.EventFunction=EventFunction -Event.Params=arg -return self -end -do -function EVENT:OnBirthForTemplate(EventTemplate,EventFunction,EventClass) -self:F2(EventTemplate.name) -self:OnEventForTemplate(EventTemplate,EventFunction,EventClass,EVENTS.Birth) -return self -end -end -do -function EVENT:OnCrashForTemplate(EventTemplate,EventFunction,EventClass) -self:F2(EventTemplate.name) -self:OnEventForTemplate(EventTemplate,EventFunction,EventClass,EVENTS.Crash) -return self -end -end -do -function EVENT:OnDeadForTemplate(EventTemplate,EventFunction,EventClass) -self:F2(EventTemplate.name) -self:OnEventForTemplate(EventTemplate,EventFunction,EventClass,EVENTS.Dead) -return self -end -end -do -function EVENT:OnLandForTemplate(EventTemplate,EventFunction,EventClass) -self:F2(EventTemplate.name) -self:OnEventForTemplate(EventTemplate,EventFunction,EventClass,EVENTS.Land) -return self -end -end -do -function EVENT:OnTakeOffForTemplate(EventTemplate,EventFunction,EventClass) -self:F2(EventTemplate.name) -self:OnEventForTemplate(EventTemplate,EventFunction,EventClass,EVENTS.Takeoff) -return self -end -end -do -function EVENT:OnEngineShutDownForTemplate(EventTemplate,EventFunction,EventClass) -self:F2(EventTemplate.name) -self:OnEventForTemplate(EventTemplate,EventFunction,EventClass,EVENTS.EngineShutdown) -return self -end -end -do -function EVENT:CreateEventNewCargo(Cargo) -self:F({Cargo}) -local Event={ -id=EVENTS.NewCargo, -time=timer.getTime(), -cargo=Cargo, -} -world.onEvent(Event) -end -function EVENT:CreateEventDeleteCargo(Cargo) -self:F({Cargo}) -local Event={ -id=EVENTS.DeleteCargo, -time=timer.getTime(), -cargo=Cargo, -} -world.onEvent(Event) -end -function EVENT:CreateEventPlayerEnterUnit(PlayerUnit) -self:F({PlayerUnit}) -local Event={ -id=EVENTS.PlayerEnterUnit, -time=timer.getTime(), -initiator=PlayerUnit:GetDCSObject() -} -world.onEvent(Event) -end -end -function EVENT:onEvent(Event) -local ErrorHandler=function(errmsg) -env.info("Error in SCHEDULER function:"..errmsg) -if debug~=nil then -env.info(debug.traceback()) -end -return errmsg -end -local EventMeta=_EVENTMETA[Event.id] -if self and -self.Events and -self.Events[Event.id]and -(Event.initiator~=nil or(Event.initiator==nil and Event.id~=EVENTS.PlayerLeaveUnit))then -if Event.initiator then -Event.IniObjectCategory=Event.initiator:getCategory() -if Event.IniObjectCategory==Object.Category.UNIT then -Event.IniDCSUnit=Event.initiator -Event.IniDCSUnitName=Event.IniDCSUnit:getName() -Event.IniUnitName=Event.IniDCSUnitName -Event.IniDCSGroup=Event.IniDCSUnit:getGroup() -Event.IniUnit=UNIT:FindByName(Event.IniDCSUnitName) -if not Event.IniUnit then -Event.IniUnit=CLIENT:FindByName(Event.IniDCSUnitName,'',true) -end -Event.IniDCSGroupName="" -if Event.IniDCSGroup and Event.IniDCSGroup:isExist()then -Event.IniDCSGroupName=Event.IniDCSGroup:getName() -Event.IniGroup=GROUP:FindByName(Event.IniDCSGroupName) -if Event.IniGroup then -Event.IniGroupName=Event.IniDCSGroupName -end -end -Event.IniPlayerName=Event.IniDCSUnit:getPlayerName() -Event.IniCoalition=Event.IniDCSUnit:getCoalition() -Event.IniTypeName=Event.IniDCSUnit:getTypeName() -Event.IniCategory=Event.IniDCSUnit:getDesc().category -end -if Event.IniObjectCategory==Object.Category.STATIC then -Event.IniDCSUnit=Event.initiator -Event.IniDCSUnitName=Event.IniDCSUnit:getName() -Event.IniUnitName=Event.IniDCSUnitName -Event.IniUnit=STATIC:FindByName(Event.IniDCSUnitName,false) -Event.IniCoalition=Event.IniDCSUnit:getCoalition() -Event.IniCategory=Event.IniDCSUnit:getDesc().category -Event.IniTypeName=Event.IniDCSUnit:getTypeName() -end -if Event.IniObjectCategory==Object.Category.SCENERY then -Event.IniDCSUnit=Event.initiator -Event.IniDCSUnitName=Event.IniDCSUnit:getName() -Event.IniUnitName=Event.IniDCSUnitName -Event.IniUnit=SCENERY:Register(Event.IniDCSUnitName,Event.initiator) -Event.IniCategory=Event.IniDCSUnit:getDesc().category -Event.IniTypeName=Event.initiator:isExist()and Event.IniDCSUnit:getTypeName()or"SCENERY" -end -end -if Event.target then -Event.TgtObjectCategory=Event.target:getCategory() -if Event.TgtObjectCategory==Object.Category.UNIT then -Event.TgtDCSUnit=Event.target -Event.TgtDCSGroup=Event.TgtDCSUnit:getGroup() -Event.TgtDCSUnitName=Event.TgtDCSUnit:getName() -Event.TgtUnitName=Event.TgtDCSUnitName -Event.TgtUnit=UNIT:FindByName(Event.TgtDCSUnitName) -Event.TgtDCSGroupName="" -if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist()then -Event.TgtDCSGroupName=Event.TgtDCSGroup:getName() -Event.TgtGroup=GROUP:FindByName(Event.TgtDCSGroupName) -if Event.TgtGroup then -Event.TgtGroupName=Event.TgtDCSGroupName -end -end -Event.TgtPlayerName=Event.TgtDCSUnit:getPlayerName() -Event.TgtCoalition=Event.TgtDCSUnit:getCoalition() -Event.TgtCategory=Event.TgtDCSUnit:getDesc().category -Event.TgtTypeName=Event.TgtDCSUnit:getTypeName() -end -if Event.TgtObjectCategory==Object.Category.STATIC then -Event.TgtDCSUnit=Event.target -Event.TgtDCSUnitName=Event.TgtDCSUnit:getName() -Event.TgtUnitName=Event.TgtDCSUnitName -Event.TgtUnit=STATIC:FindByName(Event.TgtDCSUnitName) -Event.TgtCoalition=Event.TgtDCSUnit:getCoalition() -Event.TgtCategory=Event.TgtDCSUnit:getDesc().category -Event.TgtTypeName=Event.TgtDCSUnit:getTypeName() -end -if Event.TgtObjectCategory==Object.Category.SCENERY then -Event.TgtDCSUnit=Event.target -Event.TgtDCSUnitName=Event.TgtDCSUnit:getName() -Event.TgtUnitName=Event.TgtDCSUnitName -Event.TgtUnit=SCENERY:Register(Event.TgtDCSUnitName,Event.target) -Event.TgtCategory=Event.TgtDCSUnit:getDesc().category -Event.TgtTypeName=Event.TgtDCSUnit:getTypeName() -end -end -if Event.weapon then -Event.Weapon=Event.weapon -Event.WeaponName=Event.Weapon:getTypeName() -Event.WeaponUNIT=CLIENT:Find(Event.Weapon,'',true) -Event.WeaponPlayerName=Event.WeaponUNIT and Event.Weapon:getPlayerName() -Event.WeaponCoalition=Event.WeaponUNIT and Event.Weapon:getCoalition() -Event.WeaponCategory=Event.WeaponUNIT and Event.Weapon:getDesc().category -Event.WeaponTypeName=Event.WeaponUNIT and Event.Weapon:getTypeName() -end -if Event.cargo then -Event.Cargo=Event.cargo -Event.CargoName=Event.cargo.Name -end -local PriorityOrder=EventMeta.Order -local PriorityBegin=PriorityOrder==-1 and 5 or 1 -local PriorityEnd=PriorityOrder==-1 and 1 or 5 -if Event.IniObjectCategory~=Object.Category.STATIC then -self:E({EventMeta.Text,Event,Event.IniDCSUnitName,Event.TgtDCSUnitName,PriorityOrder}) -end -for EventPriority=PriorityBegin,PriorityEnd,PriorityOrder do -if self.Events[Event.id][EventPriority]then -for EventClass,EventData in pairs(self.Events[Event.id][EventPriority])do -Event.IniGroup=GROUP:FindByName(Event.IniDCSGroupName) -Event.TgtGroup=GROUP:FindByName(Event.TgtDCSGroupName) -if EventData.EventUnit then -if EventClass:IsAlive()or -Event.id==EVENTS.Crash or -Event.id==EVENTS.Dead then -local UnitName=EventClass:GetName() -if(EventMeta.Side=="I"and UnitName==Event.IniDCSUnitName)or -(EventMeta.Side=="T"and UnitName==Event.TgtDCSUnitName)then -if EventData.EventFunction then -if Event.IniObjectCategory~=3 then -self:E({"Calling EventFunction for UNIT ",EventClass:GetClassNameAndID(),", Unit ",Event.IniUnitName,EventPriority}) -end -local Result,Value=xpcall( -function() -return EventData.EventFunction(EventClass,Event) -end,ErrorHandler) -else -local EventFunction=EventClass[EventMeta.Event] -if EventFunction and type(EventFunction)=="function"then -if Event.IniObjectCategory~=3 then -self:E({"Calling "..EventMeta.Event.." for Class ",EventClass:GetClassNameAndID(),EventPriority}) -end -local Result,Value=xpcall( -function() -return EventFunction(EventClass,Event) -end,ErrorHandler) -end -end -end -else -self:RemoveEvent(EventClass,Event.id) -end -else -if EventData.EventGroup then -if EventClass:IsAlive()or -Event.id==EVENTS.Crash or -Event.id==EVENTS.Dead then -local GroupName=EventClass:GetName() -if(EventMeta.Side=="I"and GroupName==Event.IniDCSGroupName)or -(EventMeta.Side=="T"and GroupName==Event.TgtDCSGroupName)then -if EventData.EventFunction then -if Event.IniObjectCategory~=3 then -self:E({"Calling EventFunction for GROUP ",EventClass:GetClassNameAndID(),", Unit ",Event.IniUnitName,EventPriority}) -end -local Result,Value=xpcall( -function() -return EventData.EventFunction(EventClass,Event,unpack(EventData.Params)) -end,ErrorHandler) -else -local EventFunction=EventClass[EventMeta.Event] -if EventFunction and type(EventFunction)=="function"then -if Event.IniObjectCategory~=3 then -self:E({"Calling "..EventMeta.Event.." for GROUP ",EventClass:GetClassNameAndID(),EventPriority}) -end -local Result,Value=xpcall( -function() -return EventFunction(EventClass,Event,unpack(EventData.Params)) -end,ErrorHandler) -end -end -end -else -end -else -if not EventData.EventUnit then -if EventData.EventFunction then -if Event.IniObjectCategory~=3 then -self:F2({"Calling EventFunction for Class ",EventClass:GetClassNameAndID(),EventPriority}) -end -local Result,Value=xpcall( -function() -return EventData.EventFunction(EventClass,Event) -end,ErrorHandler) -else -local EventFunction=EventClass[EventMeta.Event] -if EventFunction and type(EventFunction)=="function"then -if Event.IniObjectCategory~=3 then -self:F2({"Calling "..EventMeta.Event.." for Class ",EventClass:GetClassNameAndID(),EventPriority}) -end -local Result,Value=xpcall( -function() -local Result,Value=EventFunction(EventClass,Event) -return Result,Value -end,ErrorHandler) -end -end -end -end -end -end -end -end -else -self:E({EventMeta.Text,Event}) -end -Event=nil -end -EVENTHANDLER={ -ClassName="EVENTHANDLER", -ClassID=0, -} -function EVENTHANDLER:New() -self=BASE:Inherit(self,BASE:New()) -return self -end -SETTINGS={ -ClassName="SETTINGS", -} -do -function SETTINGS:Set(PlayerName) -if PlayerName==nil then -local self=BASE:Inherit(self,BASE:New()) -self:SetMetric() -self:SetA2G_BR() -self:SetA2A_BRAA() -self:SetLL_Accuracy(3) -self:SetMGRS_Accuracy(5) -self:SetMessageTime(MESSAGE.Type.Briefing,180) -self:SetMessageTime(MESSAGE.Type.Detailed,60) -self:SetMessageTime(MESSAGE.Type.Information,30) -self:SetMessageTime(MESSAGE.Type.Overview,60) -self:SetMessageTime(MESSAGE.Type.Update,15) -return self -else -local Settings=_DATABASE:GetPlayerSettings(PlayerName) -if not Settings then -Settings=BASE:Inherit(self,BASE:New()) -_DATABASE:SetPlayerSettings(PlayerName,Settings) -end -return Settings -end -end -function SETTINGS:SetMetric() -self.Metric=true -end -function SETTINGS:IsMetric() -return(self.Metric~=nil and self.Metric==true)or(self.Metric==nil and _SETTINGS:IsMetric()) -end -function SETTINGS:SetImperial() -self.Metric=false -end -function SETTINGS:IsImperial() -return(self.Metric~=nil and self.Metric==false)or(self.Metric==nil and _SETTINGS:IsMetric()) -end -function SETTINGS:SetLL_Accuracy(LL_Accuracy) -self.LL_Accuracy=LL_Accuracy -end -function SETTINGS:GetLL_DDM_Accuracy() -return self.LL_DDM_Accuracy or _SETTINGS:GetLL_DDM_Accuracy() -end -function SETTINGS:SetMGRS_Accuracy(MGRS_Accuracy) -self.MGRS_Accuracy=MGRS_Accuracy -end -function SETTINGS:GetMGRS_Accuracy() -return self.MGRS_Accuracy or _SETTINGS:GetMGRS_Accuracy() -end -function SETTINGS:SetMessageTime(MessageType,MessageTime) -self.MessageTypeTimings=self.MessageTypeTimings or{} -self.MessageTypeTimings[MessageType]=MessageTime -end -function SETTINGS:GetMessageTime(MessageType) -return(self.MessageTypeTimings and self.MessageTypeTimings[MessageType])or _SETTINGS:GetMessageTime(MessageType) -end -function SETTINGS:SetA2G_LL_DMS() -self.A2GSystem="LL DMS" -end -function SETTINGS:SetA2G_LL_DDM() -self.A2GSystem="LL DDM" -end -function SETTINGS:IsA2G_LL_DMS() -return(self.A2GSystem and self.A2GSystem=="LL DMS")or(not self.A2GSystem and _SETTINGS:IsA2G_LL_DMS()) -end -function SETTINGS:IsA2G_LL_DDM() -return(self.A2GSystem and self.A2GSystem=="LL DDM")or(not self.A2GSystem and _SETTINGS:IsA2G_LL_DDM()) -end -function SETTINGS:SetA2G_MGRS() -self.A2GSystem="MGRS" -end -function SETTINGS:IsA2G_MGRS() -return(self.A2GSystem and self.A2GSystem=="MGRS")or(not self.A2GSystem and _SETTINGS:IsA2G_MGRS()) -end -function SETTINGS:SetA2G_BR() -self.A2GSystem="BR" -end -function SETTINGS:IsA2G_BR() -return(self.A2GSystem and self.A2GSystem=="BR")or(not self.A2GSystem and _SETTINGS:IsA2G_BR()) -end -function SETTINGS:SetA2A_BRAA() -self.A2ASystem="BRAA" -end -function SETTINGS:IsA2A_BRAA() -self:E({BRA=(self.A2ASystem and self.A2ASystem=="BRAA")or(not self.A2ASystem and _SETTINGS:IsA2A_BRAA())}) -return(self.A2ASystem and self.A2ASystem=="BRAA")or(not self.A2ASystem and _SETTINGS:IsA2A_BRAA()) -end -function SETTINGS:SetA2A_BULLS() -self.A2ASystem="BULLS" -end -function SETTINGS:IsA2A_BULLS() -return(self.A2ASystem and self.A2ASystem=="BULLS")or(not self.A2ASystem and _SETTINGS:IsA2A_BULLS()) -end -function SETTINGS:SetA2A_LL_DMS() -self.A2ASystem="LL DMS" -end -function SETTINGS:SetA2A_LL_DDM() -self.A2ASystem="LL DDM" -end -function SETTINGS:IsA2A_LL_DMS() -return(self.A2ASystem and self.A2ASystem=="LL DMS")or(not self.A2ASystem and _SETTINGS:IsA2A_LL_DMS()) -end -function SETTINGS:IsA2A_LL_DDM() -return(self.A2ASystem and self.A2ASystem=="LL DDM")or(not self.A2ASystem and _SETTINGS:IsA2A_LL_DDM()) -end -function SETTINGS:SetA2A_MGRS() -self.A2ASystem="MGRS" -end -function SETTINGS:IsA2A_MGRS() -return(self.A2ASystem and self.A2ASystem=="MGRS")or(not self.A2ASystem and _SETTINGS:IsA2A_MGRS()) -end -function SETTINGS:SetSystemMenu(MenuGroup,RootMenu) -local MenuText="System Settings" -local MenuTime=timer.getTime() -local SettingsMenu=MENU_GROUP:New(MenuGroup,MenuText,RootMenu):SetTime(MenuTime) -local A2GCoordinateMenu=MENU_GROUP:New(MenuGroup,"A2G Coordinate System",SettingsMenu):SetTime(MenuTime) -if not self:IsA2G_LL_DMS()then -MENU_GROUP_COMMAND:New(MenuGroup,"Lat/Lon Degree Min Sec (LL DMS)",A2GCoordinateMenu,self.A2GMenuSystem,self,MenuGroup,RootMenu,"LL DMS"):SetTime(MenuTime) -end -if not self:IsA2G_LL_DDM()then -MENU_GROUP_COMMAND:New(MenuGroup,"Lat/Lon Degree Dec Min (LL DDM)",A2GCoordinateMenu,self.A2GMenuSystem,self,MenuGroup,RootMenu,"LL DDM"):SetTime(MenuTime) -end -if self:IsA2G_LL_DDM()then -MENU_GROUP_COMMAND:New(MenuGroup,"LL DDM Accuracy 1",A2GCoordinateMenu,self.MenuLL_DDM_Accuracy,self,MenuGroup,RootMenu,1):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"LL DDM Accuracy 2",A2GCoordinateMenu,self.MenuLL_DDM_Accuracy,self,MenuGroup,RootMenu,2):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"LL DDM Accuracy 3",A2GCoordinateMenu,self.MenuLL_DDM_Accuracy,self,MenuGroup,RootMenu,3):SetTime(MenuTime) -end -if not self:IsA2G_BR()then -MENU_GROUP_COMMAND:New(MenuGroup,"Bearing, Range (BR)",A2GCoordinateMenu,self.A2GMenuSystem,self,MenuGroup,RootMenu,"BR"):SetTime(MenuTime) -end -if not self:IsA2G_MGRS()then -MENU_GROUP_COMMAND:New(MenuGroup,"Military Grid (MGRS)",A2GCoordinateMenu,self.A2GMenuSystem,self,MenuGroup,RootMenu,"MGRS"):SetTime(MenuTime) -end -if self:IsA2G_MGRS()then -MENU_GROUP_COMMAND:New(MenuGroup,"MGRS Accuracy 1",A2GCoordinateMenu,self.MenuMGRS_Accuracy,self,MenuGroup,RootMenu,1):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"MGRS Accuracy 2",A2GCoordinateMenu,self.MenuMGRS_Accuracy,self,MenuGroup,RootMenu,2):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"MGRS Accuracy 3",A2GCoordinateMenu,self.MenuMGRS_Accuracy,self,MenuGroup,RootMenu,3):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"MGRS Accuracy 4",A2GCoordinateMenu,self.MenuMGRS_Accuracy,self,MenuGroup,RootMenu,4):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"MGRS Accuracy 5",A2GCoordinateMenu,self.MenuMGRS_Accuracy,self,MenuGroup,RootMenu,5):SetTime(MenuTime) -end -local A2ACoordinateMenu=MENU_GROUP:New(MenuGroup,"A2A Coordinate System",SettingsMenu):SetTime(MenuTime) -if not self:IsA2A_LL_DMS()then -MENU_GROUP_COMMAND:New(MenuGroup,"Lat/Lon Degree Min Sec (LL DMS)",A2ACoordinateMenu,self.A2AMenuSystem,self,MenuGroup,RootMenu,"LL DMS"):SetTime(MenuTime) -end -if not self:IsA2A_LL_DDM()then -MENU_GROUP_COMMAND:New(MenuGroup,"Lat/Lon Degree Dec Min (LL DDM)",A2ACoordinateMenu,self.A2AMenuSystem,self,MenuGroup,RootMenu,"LL DDM"):SetTime(MenuTime) -end -if self:IsA2A_LL_DDM()then -MENU_GROUP_COMMAND:New(MenuGroup,"LL DDM Accuracy 1",A2ACoordinateMenu,self.MenuLL_DDM_Accuracy,self,MenuGroup,RootMenu,1):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"LL DDM Accuracy 2",A2ACoordinateMenu,self.MenuLL_DDM_Accuracy,self,MenuGroup,RootMenu,2):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"LL DDM Accuracy 3",A2ACoordinateMenu,self.MenuLL_DDM_Accuracy,self,MenuGroup,RootMenu,3):SetTime(MenuTime) -end -if not self:IsA2A_BULLS()then -MENU_GROUP_COMMAND:New(MenuGroup,"Bullseye (BULLS)",A2ACoordinateMenu,self.A2AMenuSystem,self,MenuGroup,RootMenu,"BULLS"):SetTime(MenuTime) -end -if not self:IsA2A_BRAA()then -MENU_GROUP_COMMAND:New(MenuGroup,"Bearing Range Altitude Aspect (BRAA)",A2ACoordinateMenu,self.A2AMenuSystem,self,MenuGroup,RootMenu,"BRAA"):SetTime(MenuTime) -end -if not self:IsA2A_MGRS()then -MENU_GROUP_COMMAND:New(MenuGroup,"Military Grid (MGRS)",A2ACoordinateMenu,self.A2AMenuSystem,self,MenuGroup,RootMenu,"MGRS"):SetTime(MenuTime) -end -if self:IsA2A_MGRS()then -MENU_GROUP_COMMAND:New(MenuGroup,"MGRS Accuracy 1",A2ACoordinateMenu,self.MenuMGRS_Accuracy,self,MenuGroup,RootMenu,1):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"MGRS Accuracy 2",A2ACoordinateMenu,self.MenuMGRS_Accuracy,self,MenuGroup,RootMenu,2):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"MGRS Accuracy 3",A2ACoordinateMenu,self.MenuMGRS_Accuracy,self,MenuGroup,RootMenu,3):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"MGRS Accuracy 4",A2ACoordinateMenu,self.MenuMGRS_Accuracy,self,MenuGroup,RootMenu,4):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"MGRS Accuracy 5",A2ACoordinateMenu,self.MenuMGRS_Accuracy,self,MenuGroup,RootMenu,5):SetTime(MenuTime) -end -local MetricsMenu=MENU_GROUP:New(MenuGroup,"Measures and Weights System",SettingsMenu):SetTime(MenuTime) -if self:IsMetric()then -MENU_GROUP_COMMAND:New(MenuGroup,"Imperial (Miles,Feet)",MetricsMenu,self.MenuMWSystem,self,MenuGroup,RootMenu,false):SetTime(MenuTime) -end -if self:IsImperial()then -MENU_GROUP_COMMAND:New(MenuGroup,"Metric (Kilometers,Meters)",MetricsMenu,self.MenuMWSystem,self,MenuGroup,RootMenu,true):SetTime(MenuTime) -end -local MessagesMenu=MENU_GROUP:New(MenuGroup,"Messages and Reports",SettingsMenu):SetTime(MenuTime) -local UpdateMessagesMenu=MENU_GROUP:New(MenuGroup,"Update Messages",MessagesMenu):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"Off",UpdateMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Update,0):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"5 seconds",UpdateMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Update,5):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"10 seconds",UpdateMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Update,10):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"15 seconds",UpdateMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Update,15):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"30 seconds",UpdateMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Update,30):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"1 minute",UpdateMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Update,60):SetTime(MenuTime) -local InformationMessagesMenu=MENU_GROUP:New(MenuGroup,"Information Messages",MessagesMenu):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"5 seconds",InformationMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Information,5):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"10 seconds",InformationMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Information,10):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"15 seconds",InformationMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Information,15):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"30 seconds",InformationMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Information,30):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"1 minute",InformationMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Information,60):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"2 minutes",InformationMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Information,120):SetTime(MenuTime) -local BriefingReportsMenu=MENU_GROUP:New(MenuGroup,"Briefing Reports",MessagesMenu):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"15 seconds",BriefingReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Briefing,15):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"30 seconds",BriefingReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Briefing,30):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"1 minute",BriefingReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Briefing,60):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"2 minutes",BriefingReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Briefing,120):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"3 minutes",BriefingReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Briefing,180):SetTime(MenuTime) -local OverviewReportsMenu=MENU_GROUP:New(MenuGroup,"Overview Reports",MessagesMenu):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"15 seconds",OverviewReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Overview,15):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"30 seconds",OverviewReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Overview,30):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"1 minute",OverviewReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Overview,60):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"2 minutes",OverviewReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Overview,120):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"3 minutes",OverviewReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Overview,180):SetTime(MenuTime) -local DetailedReportsMenu=MENU_GROUP:New(MenuGroup,"Detailed Reports",MessagesMenu):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"15 seconds",DetailedReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.DetailedReportsMenu,15):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"30 seconds",DetailedReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.DetailedReportsMenu,30):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"1 minute",DetailedReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.DetailedReportsMenu,60):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"2 minutes",DetailedReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.DetailedReportsMenu,120):SetTime(MenuTime) -MENU_GROUP_COMMAND:New(MenuGroup,"3 minutes",DetailedReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.DetailedReportsMenu,180):SetTime(MenuTime) -SettingsMenu:Remove(MenuTime) -return self -end -function SETTINGS:SetPlayerMenu(PlayerUnit) -local PlayerGroup=PlayerUnit:GetGroup() -local PlayerName=PlayerUnit:GetPlayerName() -local PlayerNames=PlayerGroup:GetPlayerNames() -local PlayerMenu=MENU_GROUP:New(PlayerGroup,'Settings "'..PlayerName..'"') -self.PlayerMenu=PlayerMenu -local A2GCoordinateMenu=MENU_GROUP:New(PlayerGroup,"A2G Coordinate System",PlayerMenu) -if not self:IsA2G_LL_DMS()then -MENU_GROUP_COMMAND:New(PlayerGroup,"Lat/Lon Degree Min Sec (LL DMS)",A2GCoordinateMenu,self.MenuGroupA2GSystem,self,PlayerUnit,PlayerGroup,PlayerName,"LL DMS") -end -if not self:IsA2G_LL_DDM()then -MENU_GROUP_COMMAND:New(PlayerGroup,"Lat/Lon Degree Dec Min (LL DDM)",A2GCoordinateMenu,self.MenuGroupA2GSystem,self,PlayerUnit,PlayerGroup,PlayerName,"LL DDM") -end -if self:IsA2G_LL_DDM()then -MENU_GROUP_COMMAND:New(PlayerGroup,"LL DDM Accuracy 1",A2GCoordinateMenu,self.MenuGroupLL_DDM_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,1) -MENU_GROUP_COMMAND:New(PlayerGroup,"LL DDM Accuracy 2",A2GCoordinateMenu,self.MenuGroupLL_DDM_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,2) -MENU_GROUP_COMMAND:New(PlayerGroup,"LL DDM Accuracy 3",A2GCoordinateMenu,self.MenuGroupLL_DDM_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,3) -end -if not self:IsA2G_BR()then -MENU_GROUP_COMMAND:New(PlayerGroup,"Bearing, Range (BR)",A2GCoordinateMenu,self.MenuGroupA2GSystem,self,PlayerUnit,PlayerGroup,PlayerName,"BR") -end -if not self:IsA2G_MGRS()then -MENU_GROUP_COMMAND:New(PlayerGroup,"Military Grid (MGRS)",A2GCoordinateMenu,self.MenuGroupA2GSystem,self,PlayerUnit,PlayerGroup,PlayerName,"MGRS") -end -if self:IsA2G_MGRS()then -MENU_GROUP_COMMAND:New(PlayerGroup,"MGRS Accuracy 1",A2GCoordinateMenu,self.MenuGroupMGRS_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,1) -MENU_GROUP_COMMAND:New(PlayerGroup,"MGRS Accuracy 2",A2GCoordinateMenu,self.MenuGroupMGRS_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,2) -MENU_GROUP_COMMAND:New(PlayerGroup,"MGRS Accuracy 3",A2GCoordinateMenu,self.MenuGroupMGRS_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,3) -MENU_GROUP_COMMAND:New(PlayerGroup,"MGRS Accuracy 4",A2GCoordinateMenu,self.MenuGroupMGRS_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,4) -MENU_GROUP_COMMAND:New(PlayerGroup,"MGRS Accuracy 5",A2GCoordinateMenu,self.MenuGroupMGRS_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,5) -end -local A2ACoordinateMenu=MENU_GROUP:New(PlayerGroup,"A2A Coordinate System",PlayerMenu) -if not self:IsA2A_LL_DMS()then -MENU_GROUP_COMMAND:New(PlayerGroup,"Lat/Lon Degree Min Sec (LL DMS)",A2GCoordinateMenu,self.MenuGroupA2GSystem,self,PlayerUnit,PlayerGroup,PlayerName,"LL DMS") -end -if not self:IsA2A_LL_DDM()then -MENU_GROUP_COMMAND:New(PlayerGroup,"Lat/Lon Degree Dec Min (LL DDM)",A2GCoordinateMenu,self.MenuGroupA2GSystem,self,PlayerUnit,PlayerGroup,PlayerName,"LL DDM") -end -if self:IsA2A_LL_DDM()then -MENU_GROUP_COMMAND:New(PlayerGroup,"LL DDM Accuracy 1",A2GCoordinateMenu,self.MenuGroupLL_DDM_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,1) -MENU_GROUP_COMMAND:New(PlayerGroup,"LL DDM Accuracy 2",A2GCoordinateMenu,self.MenuGroupLL_DDM_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,2) -MENU_GROUP_COMMAND:New(PlayerGroup,"LL DDM Accuracy 3",A2GCoordinateMenu,self.MenuGroupLL_DDM_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,3) -end -if not self:IsA2A_BULLS()then -MENU_GROUP_COMMAND:New(PlayerGroup,"Bullseye (BULLS)",A2ACoordinateMenu,self.MenuGroupA2ASystem,self,PlayerUnit,PlayerGroup,PlayerName,"BULLS") -end -if not self:IsA2A_BRAA()then -MENU_GROUP_COMMAND:New(PlayerGroup,"Bearing Range Altitude Aspect (BRAA)",A2ACoordinateMenu,self.MenuGroupA2ASystem,self,PlayerUnit,PlayerGroup,PlayerName,"BRAA") -end -if not self:IsA2A_MGRS()then -MENU_GROUP_COMMAND:New(PlayerGroup,"Military Grid (MGRS)",A2ACoordinateMenu,self.MenuGroupA2ASystem,self,PlayerUnit,PlayerGroup,PlayerName,"MGRS") -end -if self:IsA2A_MGRS()then -MENU_GROUP_COMMAND:New(PlayerGroup,"Military Grid (MGRS) Accuracy 1",A2ACoordinateMenu,self.MenuGroupMGRS_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,1) -MENU_GROUP_COMMAND:New(PlayerGroup,"Military Grid (MGRS) Accuracy 2",A2ACoordinateMenu,self.MenuGroupMGRS_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,2) -MENU_GROUP_COMMAND:New(PlayerGroup,"Military Grid (MGRS) Accuracy 3",A2ACoordinateMenu,self.MenuGroupMGRS_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,3) -MENU_GROUP_COMMAND:New(PlayerGroup,"Military Grid (MGRS) Accuracy 4",A2ACoordinateMenu,self.MenuGroupMGRS_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,4) -MENU_GROUP_COMMAND:New(PlayerGroup,"Military Grid (MGRS) Accuracy 5",A2ACoordinateMenu,self.MenuGroupMGRS_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,5) -end -local MetricsMenu=MENU_GROUP:New(PlayerGroup,"Measures and Weights System",PlayerMenu) -if self:IsMetric()then -MENU_GROUP_COMMAND:New(PlayerGroup,"Imperial (Miles,Feet)",MetricsMenu,self.MenuGroupMWSystem,self,PlayerUnit,PlayerGroup,PlayerName,false) -end -if self:IsImperial()then -MENU_GROUP_COMMAND:New(PlayerGroup,"Metric (Kilometers,Meters)",MetricsMenu,self.MenuGroupMWSystem,self,PlayerUnit,PlayerGroup,PlayerName,true) -end -local MessagesMenu=MENU_GROUP:New(PlayerGroup,"Messages and Reports",PlayerMenu) -local UpdateMessagesMenu=MENU_GROUP:New(PlayerGroup,"Update Messages",MessagesMenu) -MENU_GROUP_COMMAND:New(PlayerGroup,"Off",UpdateMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Update,0) -MENU_GROUP_COMMAND:New(PlayerGroup,"5 seconds",UpdateMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Update,5) -MENU_GROUP_COMMAND:New(PlayerGroup,"10 seconds",UpdateMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Update,10) -MENU_GROUP_COMMAND:New(PlayerGroup,"15 seconds",UpdateMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Update,15) -MENU_GROUP_COMMAND:New(PlayerGroup,"30 seconds",UpdateMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Update,30) -MENU_GROUP_COMMAND:New(PlayerGroup,"1 minute",UpdateMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Update,60) -local InformationMessagesMenu=MENU_GROUP:New(PlayerGroup,"Information Messages",MessagesMenu) -MENU_GROUP_COMMAND:New(PlayerGroup,"5 seconds",InformationMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Information,5) -MENU_GROUP_COMMAND:New(PlayerGroup,"10 seconds",InformationMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Information,10) -MENU_GROUP_COMMAND:New(PlayerGroup,"15 seconds",InformationMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Information,15) -MENU_GROUP_COMMAND:New(PlayerGroup,"30 seconds",InformationMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Information,30) -MENU_GROUP_COMMAND:New(PlayerGroup,"1 minute",InformationMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Information,60) -MENU_GROUP_COMMAND:New(PlayerGroup,"2 minutes",InformationMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Information,120) -local BriefingReportsMenu=MENU_GROUP:New(PlayerGroup,"Briefing Reports",MessagesMenu) -MENU_GROUP_COMMAND:New(PlayerGroup,"15 seconds",BriefingReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Briefing,15) -MENU_GROUP_COMMAND:New(PlayerGroup,"30 seconds",BriefingReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Briefing,30) -MENU_GROUP_COMMAND:New(PlayerGroup,"1 minute",BriefingReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Briefing,60) -MENU_GROUP_COMMAND:New(PlayerGroup,"2 minutes",BriefingReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Briefing,120) -MENU_GROUP_COMMAND:New(PlayerGroup,"3 minutes",BriefingReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Briefing,180) -local OverviewReportsMenu=MENU_GROUP:New(PlayerGroup,"Overview Reports",MessagesMenu) -MENU_GROUP_COMMAND:New(PlayerGroup,"15 seconds",OverviewReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Overview,15) -MENU_GROUP_COMMAND:New(PlayerGroup,"30 seconds",OverviewReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Overview,30) -MENU_GROUP_COMMAND:New(PlayerGroup,"1 minute",OverviewReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Overview,60) -MENU_GROUP_COMMAND:New(PlayerGroup,"2 minutes",OverviewReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Overview,120) -MENU_GROUP_COMMAND:New(PlayerGroup,"3 minutes",OverviewReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Overview,180) -local DetailedReportsMenu=MENU_GROUP:New(PlayerGroup,"Detailed Reports",MessagesMenu) -MENU_GROUP_COMMAND:New(PlayerGroup,"15 seconds",DetailedReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.DetailedReportsMenu,15) -MENU_GROUP_COMMAND:New(PlayerGroup,"30 seconds",DetailedReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.DetailedReportsMenu,30) -MENU_GROUP_COMMAND:New(PlayerGroup,"1 minute",DetailedReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.DetailedReportsMenu,60) -MENU_GROUP_COMMAND:New(PlayerGroup,"2 minutes",DetailedReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.DetailedReportsMenu,120) -MENU_GROUP_COMMAND:New(PlayerGroup,"3 minutes",DetailedReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.DetailedReportsMenu,180) -return self -end -function SETTINGS:RemovePlayerMenu(PlayerUnit) -if self.PlayerMenu then -self.PlayerMenu:Remove() -end -return self -end -function SETTINGS:A2GMenuSystem(MenuGroup,RootMenu,A2GSystem) -self.A2GSystem=A2GSystem -MESSAGE:New(string.format("Settings: Default A2G coordinate system set to %s for all players!",A2GSystem),5):ToAll() -self:SetSystemMenu(MenuGroup,RootMenu) -end -function SETTINGS:A2AMenuSystem(MenuGroup,RootMenu,A2ASystem) -self.A2ASystem=A2ASystem -MESSAGE:New(string.format("Settings: Default A2A coordinate system set to %s for all players!",A2ASystem),5):ToAll() -self:SetSystemMenu(MenuGroup,RootMenu) -end -function SETTINGS:MenuLL_DDM_Accuracy(MenuGroup,RootMenu,LL_Accuracy) -self.LL_Accuracy=LL_Accuracy -MESSAGE:New(string.format("Settings: Default LL accuracy set to %s for all players!",LL_Accuracy),5):ToAll() -self:SetSystemMenu(MenuGroup,RootMenu) -end -function SETTINGS:MenuMGRS_Accuracy(MenuGroup,RootMenu,MGRS_Accuracy) -self.MGRS_Accuracy=MGRS_Accuracy -MESSAGE:New(string.format("Settings: Default MGRS accuracy set to %s for all players!",MGRS_Accuracy),5):ToAll() -self:SetSystemMenu(MenuGroup,RootMenu) -end -function SETTINGS:MenuMWSystem(MenuGroup,RootMenu,MW) -self.Metric=MW -MESSAGE:New(string.format("Settings: Default measurement format set to %s for all players!",MW and"Metric"or"Imperial"),5):ToAll() -self:SetSystemMenu(MenuGroup,RootMenu) -end -function SETTINGS:MenuMessageTimingsSystem(MenuGroup,RootMenu,MessageType,MessageTime) -self:SetMessageTime(MessageType,MessageTime) -MESSAGE:New(string.format("Settings: Default message time set for %s to %d.",MessageType,MessageTime),5):ToAll() -end -do -function SETTINGS:MenuGroupA2GSystem(PlayerUnit,PlayerGroup,PlayerName,A2GSystem) -BASE:E({self,PlayerUnit:GetName(),A2GSystem}) -self.A2GSystem=A2GSystem -MESSAGE:New(string.format("Settings: A2G format set to %s for player %s.",A2GSystem,PlayerName),5):ToGroup(PlayerGroup) -self:RemovePlayerMenu(PlayerUnit) -self:SetPlayerMenu(PlayerUnit) -end -function SETTINGS:MenuGroupA2ASystem(PlayerUnit,PlayerGroup,PlayerName,A2ASystem) -self.A2ASystem=A2ASystem -MESSAGE:New(string.format("Settings: A2A format set to %s for player %s.",A2ASystem,PlayerName),5):ToGroup(PlayerGroup) -self:RemovePlayerMenu(PlayerUnit) -self:SetPlayerMenu(PlayerUnit) -end -function SETTINGS:MenuGroupLL_DDM_AccuracySystem(PlayerUnit,PlayerGroup,PlayerName,LL_Accuracy) -self.LL_Accuracy=LL_Accuracy -MESSAGE:New(string.format("Settings: A2G LL format accuracy set to %d for player %s.",LL_Accuracy,PlayerName),5):ToGroup(PlayerGroup) -self:RemovePlayerMenu(PlayerUnit) -self:SetPlayerMenu(PlayerUnit) -end -function SETTINGS:MenuGroupMGRS_AccuracySystem(PlayerUnit,PlayerGroup,PlayerName,MGRS_Accuracy) -self.MGRS_Accuracy=MGRS_Accuracy -MESSAGE:New(string.format("Settings: A2G MGRS format accuracy set to %d for player %s.",MGRS_Accuracy,PlayerName),5):ToGroup(PlayerGroup) -self:RemovePlayerMenu(PlayerUnit) -self:SetPlayerMenu(PlayerUnit) -end -function SETTINGS:MenuGroupMWSystem(PlayerUnit,PlayerGroup,PlayerName,MW) -self.Metric=MW -MESSAGE:New(string.format("Settings: Measurement format set to %s for player %s.",MW and"Metric"or"Imperial",PlayerName),5):ToGroup(PlayerGroup) -self:RemovePlayerMenu(PlayerUnit) -self:SetPlayerMenu(PlayerUnit) -end -function SETTINGS:MenuGroupMessageTimingsSystem(PlayerUnit,PlayerGroup,PlayerName,MessageType,MessageTime) -self:SetMessageTime(MessageType,MessageTime) -MESSAGE:New(string.format("Settings: Default message time set for %s to %d.",MessageType,MessageTime),5):ToGroup(PlayerGroup) -end -end -end -do -MENU_BASE={ -ClassName="MENU_BASE", -MenuPath=nil, -MenuText="", -MenuParentPath=nil -} -function MENU_BASE:New(MenuText,ParentMenu) -local MenuParentPath={} -if ParentMenu~=nil then -MenuParentPath=ParentMenu.MenuPath -end -local self=BASE:Inherit(self,BASE:New()) -self.MenuPath=nil -self.MenuText=MenuText -self.MenuParentPath=MenuParentPath -self.Menus={} -self.MenuCount=0 -self.MenuRemoveParent=false -self.MenuTime=timer.getTime() -return self -end -function MENU_BASE:GetMenu(MenuText) -self:F2({Menu=self.Menus[MenuText]}) -return self.Menus[MenuText] -end -function MENU_BASE:SetRemoveParent(RemoveParent) -self:F2({RemoveParent}) -self.MenuRemoveParent=RemoveParent -return self -end -function MENU_BASE:SetTime(MenuTime) -self.MenuTime=MenuTime -return self -end -function MENU_BASE:SetTag(MenuTag) -self.MenuTag=MenuTag -return self -end -end -do -MENU_COMMAND_BASE={ -ClassName="MENU_COMMAND_BASE", -CommandMenuFunction=nil, -CommandMenuArgument=nil, -MenuCallHandler=nil, -} -function MENU_COMMAND_BASE:New(MenuText,ParentMenu,CommandMenuFunction,CommandMenuArguments) -local self=BASE:Inherit(self,MENU_BASE:New(MenuText,ParentMenu)) -local ErrorHandler=function(errmsg) -env.info("MOOSE error in MENU COMMAND function: "..errmsg) -if debug~=nil then -env.info(debug.traceback()) -end -return errmsg -end -self:SetCommandMenuFunction(CommandMenuFunction) -self:SetCommandMenuArguments(CommandMenuArguments) -self.MenuCallHandler=function() -local function MenuFunction() -return self.CommandMenuFunction(unpack(self.CommandMenuArguments)) -end -local Status,Result=xpcall(MenuFunction,ErrorHandler) -end -return self -end -function MENU_COMMAND_BASE:SetCommandMenuFunction(CommandMenuFunction) -self.CommandMenuFunction=CommandMenuFunction -return self -end -function MENU_COMMAND_BASE:SetCommandMenuArguments(CommandMenuArguments) -self.CommandMenuArguments=CommandMenuArguments -return self -end -end -do -MENU_MISSION={ -ClassName="MENU_MISSION" -} -function MENU_MISSION:New(MenuText,ParentMenu) -local self=BASE:Inherit(self,MENU_BASE:New(MenuText,ParentMenu)) -self:F({MenuText,ParentMenu}) -self.MenuText=MenuText -self.ParentMenu=ParentMenu -self.Menus={} -self:T({MenuText}) -self.MenuPath=missionCommands.addSubMenu(MenuText,self.MenuParentPath) -self:T({self.MenuPath}) -if ParentMenu and ParentMenu.Menus then -ParentMenu.Menus[self.MenuPath]=self -end -return self -end -function MENU_MISSION:RemoveSubMenus() -self:F(self.MenuPath) -for MenuID,Menu in pairs(self.Menus)do -Menu:Remove() -end -end -function MENU_MISSION:Remove() -self:F(self.MenuPath) -self:RemoveSubMenus() -missionCommands.removeItem(self.MenuPath) -if self.ParentMenu then -self.ParentMenu.Menus[self.MenuPath]=nil -end -return nil -end -end -do -MENU_MISSION_COMMAND={ -ClassName="MENU_MISSION_COMMAND" -} -function MENU_MISSION_COMMAND:New(MenuText,ParentMenu,CommandMenuFunction,...) -local self=BASE:Inherit(self,MENU_COMMAND_BASE:New(MenuText,ParentMenu,CommandMenuFunction,arg)) -self.MenuText=MenuText -self.ParentMenu=ParentMenu -self:T({MenuText,CommandMenuFunction,arg}) -self.MenuPath=missionCommands.addCommand(MenuText,self.MenuParentPath,self.MenuCallHandler) -ParentMenu.Menus[self.MenuPath]=self -return self -end -function MENU_MISSION_COMMAND:Remove() -self:F(self.MenuPath) -missionCommands.removeItem(self.MenuPath) -if self.ParentMenu then -self.ParentMenu.Menus[self.MenuPath]=nil -end -return nil -end -end -do -MENU_COALITION={ -ClassName="MENU_COALITION" -} -function MENU_COALITION:New(Coalition,MenuText,ParentMenu) -local self=BASE:Inherit(self,MENU_BASE:New(MenuText,ParentMenu)) -self:F({Coalition,MenuText,ParentMenu}) -self.Coalition=Coalition -self.MenuText=MenuText -self.ParentMenu=ParentMenu -self.Menus={} -self:T({MenuText}) -self.MenuPath=missionCommands.addSubMenuForCoalition(Coalition,MenuText,self.MenuParentPath) -self:T({self.MenuPath}) -if ParentMenu and ParentMenu.Menus then -ParentMenu.Menus[self.MenuPath]=self -end -return self -end -function MENU_COALITION:RemoveSubMenus() -self:F(self.MenuPath) -for MenuID,Menu in pairs(self.Menus)do -Menu:Remove() -end -end -function MENU_COALITION:Remove() -self:F(self.MenuPath) -self:RemoveSubMenus() -missionCommands.removeItemForCoalition(self.Coalition,self.MenuPath) -if self.ParentMenu then -self.ParentMenu.Menus[self.MenuPath]=nil -end -return nil -end -end -do -MENU_COALITION_COMMAND={ -ClassName="MENU_COALITION_COMMAND" -} -function MENU_COALITION_COMMAND:New(Coalition,MenuText,ParentMenu,CommandMenuFunction,...) -local self=BASE:Inherit(self,MENU_COMMAND_BASE:New(MenuText,ParentMenu,CommandMenuFunction,arg)) -self.MenuCoalition=Coalition -self.MenuText=MenuText -self.ParentMenu=ParentMenu -self:T({MenuText,CommandMenuFunction,arg}) -self.MenuPath=missionCommands.addCommandForCoalition(self.MenuCoalition,MenuText,self.MenuParentPath,self.MenuCallHandler) -ParentMenu.Menus[self.MenuPath]=self -return self -end -function MENU_COALITION_COMMAND:Remove() -self:F(self.MenuPath) -missionCommands.removeItemForCoalition(self.MenuCoalition,self.MenuPath) -if self.ParentMenu then -self.ParentMenu.Menus[self.MenuPath]=nil -end -return nil -end -end -do -local _MENUCLIENTS={} -MENU_CLIENT={ -ClassName="MENU_CLIENT" -} -function MENU_CLIENT:New(Client,MenuText,ParentMenu) -local MenuParentPath={} -if ParentMenu~=nil then -MenuParentPath=ParentMenu.MenuPath -end -local self=BASE:Inherit(self,MENU_BASE:New(MenuText,MenuParentPath)) -self:F({Client,MenuText,ParentMenu}) -self.MenuClient=Client -self.MenuClientGroupID=Client:GetClientGroupID() -self.MenuParentPath=MenuParentPath -self.MenuText=MenuText -self.ParentMenu=ParentMenu -self.Menus={} -if not _MENUCLIENTS[self.MenuClientGroupID]then -_MENUCLIENTS[self.MenuClientGroupID]={} -end -local MenuPath=_MENUCLIENTS[self.MenuClientGroupID] -self:T({Client:GetClientGroupName(),MenuPath[table.concat(MenuParentPath)],MenuParentPath,MenuText}) -local MenuPathID=table.concat(MenuParentPath).."/"..MenuText -if MenuPath[MenuPathID]then -missionCommands.removeItemForGroup(self.MenuClient:GetClientGroupID(),MenuPath[MenuPathID]) -end -self.MenuPath=missionCommands.addSubMenuForGroup(self.MenuClient:GetClientGroupID(),MenuText,MenuParentPath) -MenuPath[MenuPathID]=self.MenuPath -self:T({Client:GetClientGroupName(),self.MenuPath}) -if ParentMenu and ParentMenu.Menus then -ParentMenu.Menus[self.MenuPath]=self -end -return self -end -function MENU_CLIENT:RemoveSubMenus() -self:F(self.MenuPath) -for MenuID,Menu in pairs(self.Menus)do -Menu:Remove() -end -end -function MENU_CLIENT:Remove() -self:F(self.MenuPath) -self:RemoveSubMenus() -if not _MENUCLIENTS[self.MenuClientGroupID]then -_MENUCLIENTS[self.MenuClientGroupID]={} -end -local MenuPath=_MENUCLIENTS[self.MenuClientGroupID] -if MenuPath[table.concat(self.MenuParentPath).."/"..self.MenuText]then -MenuPath[table.concat(self.MenuParentPath).."/"..self.MenuText]=nil -end -missionCommands.removeItemForGroup(self.MenuClient:GetClientGroupID(),self.MenuPath) -self.ParentMenu.Menus[self.MenuPath]=nil -return nil -end -MENU_CLIENT_COMMAND={ -ClassName="MENU_CLIENT_COMMAND" -} -function MENU_CLIENT_COMMAND:New(Client,MenuText,ParentMenu,CommandMenuFunction,...) -local MenuParentPath={} -if ParentMenu~=nil then -MenuParentPath=ParentMenu.MenuPath -end -local self=BASE:Inherit(self,MENU_COMMAND_BASE:New(MenuText,MenuParentPath,CommandMenuFunction,arg)) -self.MenuClient=Client -self.MenuClientGroupID=Client:GetClientGroupID() -self.MenuParentPath=MenuParentPath -self.MenuText=MenuText -self.ParentMenu=ParentMenu -if not _MENUCLIENTS[self.MenuClientGroupID]then -_MENUCLIENTS[self.MenuClientGroupID]={} -end -local MenuPath=_MENUCLIENTS[self.MenuClientGroupID] -self:T({Client:GetClientGroupName(),MenuPath[table.concat(MenuParentPath)],MenuParentPath,MenuText,CommandMenuFunction,arg}) -local MenuPathID=table.concat(MenuParentPath).."/"..MenuText -if MenuPath[MenuPathID]then -missionCommands.removeItemForGroup(self.MenuClient:GetClientGroupID(),MenuPath[MenuPathID]) -end -self.MenuPath=missionCommands.addCommandForGroup(self.MenuClient:GetClientGroupID(),MenuText,MenuParentPath,self.MenuCallHandler) -MenuPath[MenuPathID]=self.MenuPath -if ParentMenu and ParentMenu.Menus then -ParentMenu.Menus[self.MenuPath]=self -end -return self -end -function MENU_CLIENT_COMMAND:Remove() -self:F(self.MenuPath) -if not _MENUCLIENTS[self.MenuClientGroupID]then -_MENUCLIENTS[self.MenuClientGroupID]={} -end -local MenuPath=_MENUCLIENTS[self.MenuClientGroupID] -if MenuPath[table.concat(self.MenuParentPath).."/"..self.MenuText]then -MenuPath[table.concat(self.MenuParentPath).."/"..self.MenuText]=nil -end -missionCommands.removeItemForGroup(self.MenuClient:GetClientGroupID(),self.MenuPath) -self.ParentMenu.Menus[self.MenuPath]=nil -return nil -end -end -do -local _MENUGROUPS={} -MENU_GROUP={ -ClassName="MENU_GROUP" -} -function MENU_GROUP:New(MenuGroup,MenuText,ParentMenu) -MenuGroup._Menus=MenuGroup._Menus or{} -local Path=(ParentMenu and(table.concat(ParentMenu.MenuPath or{},"@").."@"..MenuText))or MenuText -if MenuGroup._Menus[Path]then -self=MenuGroup._Menus[Path] -else -self=BASE:Inherit(self,MENU_BASE:New(MenuText,ParentMenu)) -MenuGroup._Menus[Path]=self -self.MenuGroup=MenuGroup -self.Path=Path -self.MenuGroupID=MenuGroup:GetID() -self.MenuText=MenuText -self.ParentMenu=ParentMenu -self:T({"Adding Menu ",MenuText,self.MenuParentPath}) -self.MenuPath=missionCommands.addSubMenuForGroup(self.MenuGroupID,MenuText,self.MenuParentPath) -if self.ParentMenu and self.ParentMenu.Menus then -self.ParentMenu.Menus[MenuText]=self -self:F({self.ParentMenu.Menus,MenuText}) -self.ParentMenu.MenuCount=self.ParentMenu.MenuCount+1 -end -end -return self -end -function MENU_GROUP:RemoveSubMenus(MenuTime,MenuTag) -self:T({"Removing Group SubMenus:",MenuTime,MenuTag,self.MenuGroup:GetName(),self.MenuPath}) -for MenuText,Menu in pairs(self.Menus)do -Menu:Remove(MenuTime,MenuTag) -end -end -function MENU_GROUP:Remove(MenuTime,MenuTag) -self:RemoveSubMenus(MenuTime,MenuTag) -if not MenuTime or self.MenuTime~=MenuTime then -if(not MenuTag)or(MenuTag and self.MenuTag and MenuTag==self.MenuTag)then -if self.MenuGroup._Menus[self.Path]then -self=self.MenuGroup._Menus[self.Path] -missionCommands.removeItemForGroup(self.MenuGroupID,self.MenuPath) -if self.ParentMenu then -self.ParentMenu.Menus[self.MenuText]=nil -self.ParentMenu.MenuCount=self.ParentMenu.MenuCount-1 -if self.ParentMenu.MenuCount==0 then -if self.MenuRemoveParent==true then -self:T2("Removing Parent Menu ") -self.ParentMenu:Remove() -end -end -end -end -self:T({"Removing Group Menu:",MenuGroup=self.MenuGroup:GetName()}) -self.MenuGroup._Menus[self.Path]=nil -self=nil -end -end -return nil -end -MENU_GROUP_COMMAND={ -ClassName="MENU_GROUP_COMMAND" -} -function MENU_GROUP_COMMAND:New(MenuGroup,MenuText,ParentMenu,CommandMenuFunction,...) -MenuGroup._Menus=MenuGroup._Menus or{} -local Path=(ParentMenu and(table.concat(ParentMenu.MenuPath or{},"@").."@"..MenuText))or MenuText -if MenuGroup._Menus[Path]then -self=MenuGroup._Menus[Path] -self:SetCommandMenuFunction(CommandMenuFunction) -self:SetCommandMenuArguments(arg) -return self -end -self=BASE:Inherit(self,MENU_COMMAND_BASE:New(MenuText,ParentMenu,CommandMenuFunction,arg)) -MenuGroup._Menus[Path]=self -self.Path=Path -self.MenuGroup=MenuGroup -self.MenuGroupID=MenuGroup:GetID() -self.MenuText=MenuText -self.ParentMenu=ParentMenu -self:F({"Adding Group Command Menu:",MenuGroup=MenuGroup:GetName(),MenuText=MenuText,MenuPath=self.MenuParentPath}) -self.MenuPath=missionCommands.addCommandForGroup(self.MenuGroupID,MenuText,self.MenuParentPath,self.MenuCallHandler) -if self.ParentMenu and self.ParentMenu.Menus then -self.ParentMenu.Menus[MenuText]=self -self.ParentMenu.MenuCount=self.ParentMenu.MenuCount+1 -self:F2({ParentMenu.Menus,MenuText}) -end -return self -end -function MENU_GROUP_COMMAND:Remove(MenuTime,MenuTag) -if not MenuTime or self.MenuTime~=MenuTime then -if(not MenuTag)or(MenuTag and self.MenuTag and MenuTag==self.MenuTag)then -if self.MenuGroup._Menus[self.Path]then -self=self.MenuGroup._Menus[self.Path] -missionCommands.removeItemForGroup(self.MenuGroupID,self.MenuPath) -self.ParentMenu.Menus[self.MenuText]=nil -self.ParentMenu.MenuCount=self.ParentMenu.MenuCount-1 -if self.ParentMenu.MenuCount==0 then -if self.MenuRemoveParent==true then -self:T2("Removing Parent Menu ") -self.ParentMenu:Remove() -end -end -self.MenuGroup._Menus[self.Path]=nil -self=nil -end -end -end -return nil -end -end -ZONE_BASE={ -ClassName="ZONE_BASE", -ZoneName="", -ZoneProbability=1, -} -function ZONE_BASE:New(ZoneName) -local self=BASE:Inherit(self,BASE:New()) -self:F(ZoneName) -self.ZoneName=ZoneName -return self -end -function ZONE_BASE:GetName() -self:F2() -return self.ZoneName -end -function ZONE_BASE:IsVec2InZone(Vec2) -self:F2(Vec2) -return false -end -function ZONE_BASE:IsVec3InZone(Vec3) -self:F2(Vec3) -local InZone=self:IsVec2InZone({x=Vec3.x,y=Vec3.z}) -return InZone -end -function ZONE_BASE:IsPointVec2InZone(PointVec2) -self:F2(PointVec2) -local InZone=self:IsVec2InZone(PointVec2:GetVec2()) -return InZone -end -function ZONE_BASE:IsPointVec3InZone(PointVec3) -self:F2(PointVec3) -local InZone=self:IsPointVec2InZone(PointVec3) -return InZone -end -function ZONE_BASE:GetVec2() -self:F2(self.ZoneName) -return nil -end -function ZONE_BASE:GetPointVec2() -self:F2(self.ZoneName) -local Vec2=self:GetVec2() -local PointVec2=POINT_VEC2:NewFromVec2(Vec2) -self:T2({PointVec2}) -return PointVec2 -end -function ZONE_BASE:GetCoordinate() -self:F2(self.ZoneName) -local Vec2=self:GetVec2() -local Coordinate=COORDINATE:NewFromVec2(Vec2) -self:T2({Coordinate}) -return Coordinate -end -function ZONE_BASE:GetVec3(Height) -self:F2(self.ZoneName) -Height=Height or 0 -local Vec2=self:GetVec2() -local Vec3={x=Vec2.x,y=Height and Height or land.getHeight(self:GetVec2()),z=Vec2.y} -self:T2({Vec3}) -return Vec3 -end -function ZONE_BASE:GetPointVec3(Height) -self:F2(self.ZoneName) -local Vec3=self:GetVec3(Height) -local PointVec3=POINT_VEC3:NewFromVec3(Vec3) -self:T2({PointVec3}) -return PointVec3 -end -function ZONE_BASE:GetCoordinate(Height) -self:F2(self.ZoneName) -local Vec3=self:GetVec3(Height) -local PointVec3=COORDINATE:NewFromVec3(Vec3) -self:T2({PointVec3}) -return PointVec3 -end -function ZONE_BASE:GetRandomVec2() -return nil -end -function ZONE_BASE:GetRandomPointVec2() -return nil -end -function ZONE_BASE:GetRandomPointVec3() -return nil -end -function ZONE_BASE:GetBoundingSquare() -return nil -end -function ZONE_BASE:BoundZone() -self:F2() -end -function ZONE_BASE:SmokeZone(SmokeColor) -self:F2(SmokeColor) -end -function ZONE_BASE:SetZoneProbability(ZoneProbability) -self:F2(ZoneProbability) -self.ZoneProbability=ZoneProbability or 1 -return self -end -function ZONE_BASE:GetZoneProbability() -self:F2() -return self.ZoneProbability -end -function ZONE_BASE:GetZoneMaybe() -self:F2() -local Randomization=math.random() -if Randomization<=self.ZoneProbability then -return self -else -return nil -end -end -ZONE_RADIUS={ -ClassName="ZONE_RADIUS", -} -function ZONE_RADIUS:New(ZoneName,Vec2,Radius) -local self=BASE:Inherit(self,ZONE_BASE:New(ZoneName)) -self:F({ZoneName,Vec2,Radius}) -self.Radius=Radius -self.Vec2=Vec2 -return self -end -function ZONE_RADIUS:BoundZone(Points,CountryID,UnBound) -local Point={} -local Vec2=self:GetVec2() -Points=Points and Points or 360 -local Angle -local RadialBase=math.pi*2 -for Angle=0,360,(360/Points)do -local Radial=Angle*RadialBase/360 -Point.x=Vec2.x+math.cos(Radial)*self:GetRadius() -Point.y=Vec2.y+math.sin(Radial)*self:GetRadius() -local CountryName=_DATABASE.COUNTRY_NAME[CountryID] -local Tire={ -["country"]=CountryName, -["category"]="Fortifications", -["canCargo"]=false, -["shape_name"]="H-tyre_B_WF", -["type"]="Black_Tyre_WF", -["y"]=Point.y, -["x"]=Point.x, -["name"]=string.format("%s-Tire #%0d",self:GetName(),Angle), -["heading"]=0, -} -local Group=coalition.addStaticObject(CountryID,Tire) -if UnBound and UnBound==true then -Group:destroy() -end -end -return self -end -function ZONE_RADIUS:SmokeZone(SmokeColor,Points) -self:F2(SmokeColor) -local Point={} -local Vec2=self:GetVec2() -Points=Points and Points or 360 -local Angle -local RadialBase=math.pi*2 -for Angle=0,360,360/Points do -local Radial=Angle*RadialBase/360 -Point.x=Vec2.x+math.cos(Radial)*self:GetRadius() -Point.y=Vec2.y+math.sin(Radial)*self:GetRadius() -POINT_VEC2:New(Point.x,Point.y):Smoke(SmokeColor) -end -return self -end -function ZONE_RADIUS:FlareZone(FlareColor,Points,Azimuth) -self:F2({FlareColor,Azimuth}) -local Point={} -local Vec2=self:GetVec2() -Points=Points and Points or 360 -local Angle -local RadialBase=math.pi*2 -for Angle=0,360,360/Points do -local Radial=Angle*RadialBase/360 -Point.x=Vec2.x+math.cos(Radial)*self:GetRadius() -Point.y=Vec2.y+math.sin(Radial)*self:GetRadius() -POINT_VEC2:New(Point.x,Point.y):Flare(FlareColor,Azimuth) -end -return self -end -function ZONE_RADIUS:GetRadius() -self:F2(self.ZoneName) -self:T2({self.Radius}) -return self.Radius -end -function ZONE_RADIUS:SetRadius(Radius) -self:F2(self.ZoneName) -self.Radius=Radius -self:T2({self.Radius}) -return self.Radius -end -function ZONE_RADIUS:GetVec2() -self:F2(self.ZoneName) -self:T2({self.Vec2}) -return self.Vec2 -end -function ZONE_RADIUS:SetVec2(Vec2) -self:F2(self.ZoneName) -self.Vec2=Vec2 -self:T2({self.Vec2}) -return self.Vec2 -end -function ZONE_RADIUS:GetVec3(Height) -self:F2({self.ZoneName,Height}) -Height=Height or 0 -local Vec2=self:GetVec2() -local Vec3={x=Vec2.x,y=land.getHeight(self:GetVec2())+Height,z=Vec2.y} -self:T2({Vec3}) -return Vec3 -end -function ZONE_RADIUS:IsVec2InZone(Vec2) -self:F2(Vec2) -local ZoneVec2=self:GetVec2() -if ZoneVec2 then -if((Vec2.x-ZoneVec2.x)^2+(Vec2.y-ZoneVec2.y)^2)^0.5<=self:GetRadius()then -return true -end -end -return false -end -function ZONE_RADIUS:IsVec3InZone(Vec3) -self:F2(Vec3) -local InZone=self:IsVec2InZone({x=Vec3.x,y=Vec3.z}) -return InZone -end -function ZONE_RADIUS:GetRandomVec2(inner,outer) -self:F(self.ZoneName,inner,outer) -local Point={} -local Vec2=self:GetVec2() -local _inner=inner or 0 -local _outer=outer or self:GetRadius() -local angle=math.random()*math.pi*2; -Point.x=Vec2.x+math.cos(angle)*math.random(_inner,_outer); -Point.y=Vec2.y+math.sin(angle)*math.random(_inner,_outer); -self:T({Point}) -return Point -end -function ZONE_RADIUS:GetRandomPointVec2(inner,outer) -self:F(self.ZoneName,inner,outer) -local PointVec2=POINT_VEC2:NewFromVec2(self:GetRandomVec2()) -self:T3({PointVec2}) -return PointVec2 -end -function ZONE_RADIUS:GetRandomPointVec3(inner,outer) -self:F(self.ZoneName,inner,outer) -local PointVec3=POINT_VEC3:NewFromVec2(self:GetRandomVec2()) -self:T3({PointVec3}) -return PointVec3 -end -function ZONE_RADIUS:GetRandomCoordinate(inner,outer) -self:F(self.ZoneName,inner,outer) -local Coordinate=COORDINATE:NewFromVec2(self:GetRandomVec2()) -self:T3({Coordinate=Coordinate}) -return Coordinate -end -ZONE={ -ClassName="ZONE", -} -function ZONE:New(ZoneName) -local Zone=trigger.misc.getZone(ZoneName) -if not Zone then -error("Zone "..ZoneName.." does not exist.") -return nil -end -local self=BASE:Inherit(self,ZONE_RADIUS:New(ZoneName,{x=Zone.point.x,y=Zone.point.z},Zone.radius)) -self:F(ZoneName) -self.Zone=Zone -return self -end -ZONE_UNIT={ -ClassName="ZONE_UNIT", -} -function ZONE_UNIT:New(ZoneName,ZoneUNIT,Radius) -local self=BASE:Inherit(self,ZONE_RADIUS:New(ZoneName,ZoneUNIT:GetVec2(),Radius)) -self:F({ZoneName,ZoneUNIT:GetVec2(),Radius}) -self.ZoneUNIT=ZoneUNIT -self.LastVec2=ZoneUNIT:GetVec2() -return self -end -function ZONE_UNIT:GetVec2() -self:F2(self.ZoneName) -local ZoneVec2=self.ZoneUNIT:GetVec2() -if ZoneVec2 then -self.LastVec2=ZoneVec2 -return ZoneVec2 -else -return self.LastVec2 -end -self:T2({ZoneVec2}) -return nil -end -function ZONE_UNIT:GetRandomVec2() -self:F(self.ZoneName) -local RandomVec2={} -local Vec2=self.ZoneUNIT:GetVec2() -if not Vec2 then -Vec2=self.LastVec2 -end -local angle=math.random()*math.pi*2; -RandomVec2.x=Vec2.x+math.cos(angle)*math.random()*self:GetRadius(); -RandomVec2.y=Vec2.y+math.sin(angle)*math.random()*self:GetRadius(); -self:T({RandomVec2}) -return RandomVec2 -end -function ZONE_UNIT:GetVec3(Height) -self:F2(self.ZoneName) -Height=Height or 0 -local Vec2=self:GetVec2() -local Vec3={x=Vec2.x,y=land.getHeight(self:GetVec2())+Height,z=Vec2.y} -self:T2({Vec3}) -return Vec3 -end -ZONE_GROUP={ -ClassName="ZONE_GROUP", -} -function ZONE_GROUP:New(ZoneName,ZoneGROUP,Radius) -local self=BASE:Inherit(self,ZONE_RADIUS:New(ZoneName,ZoneGROUP:GetVec2(),Radius)) -self:F({ZoneName,ZoneGROUP:GetVec2(),Radius}) -self._.ZoneGROUP=ZoneGROUP -return self -end -function ZONE_GROUP:GetVec2() -self:F(self.ZoneName) -local ZoneVec2=self._.ZoneGROUP:GetVec2() -self:T({ZoneVec2}) -return ZoneVec2 -end -function ZONE_GROUP:GetRandomVec2() -self:F(self.ZoneName) -local Point={} -local Vec2=self._.ZoneGROUP:GetVec2() -local angle=math.random()*math.pi*2; -Point.x=Vec2.x+math.cos(angle)*math.random()*self:GetRadius(); -Point.y=Vec2.y+math.sin(angle)*math.random()*self:GetRadius(); -self:T({Point}) -return Point -end -function ZONE_GROUP:GetRandomPointVec2(inner,outer) -self:F(self.ZoneName,inner,outer) -local PointVec2=POINT_VEC2:NewFromVec2(self:GetRandomVec2()) -self:T3({PointVec2}) -return PointVec2 -end -ZONE_POLYGON_BASE={ -ClassName="ZONE_POLYGON_BASE", -} -function ZONE_POLYGON_BASE:New(ZoneName,PointsArray) -local self=BASE:Inherit(self,ZONE_BASE:New(ZoneName)) -self:F({ZoneName,PointsArray}) -local i=0 -self._.Polygon={} -for i=1,#PointsArray do -self._.Polygon[i]={} -self._.Polygon[i].x=PointsArray[i].x -self._.Polygon[i].y=PointsArray[i].y -end -return self -end -function ZONE_POLYGON_BASE:GetVec2() -self:F(self.ZoneName) -local Bounds=self:GetBoundingSquare() -return{x=(Bounds.x2+Bounds.x1)/2,y=(Bounds.y2+Bounds.y1)/2} -end -function ZONE_POLYGON_BASE:Flush() -self:F2() -self:E({Polygon=self.ZoneName,Coordinates=self._.Polygon}) -return self -end -function ZONE_POLYGON_BASE:BoundZone(UnBound) -local i -local j -local Segments=10 -i=1 -j=#self._.Polygon -while i<=#self._.Polygon do -self:T({i,j,self._.Polygon[i],self._.Polygon[j]}) -local DeltaX=self._.Polygon[j].x-self._.Polygon[i].x -local DeltaY=self._.Polygon[j].y-self._.Polygon[i].y -for Segment=0,Segments do -local PointX=self._.Polygon[i].x+(Segment*DeltaX/Segments) -local PointY=self._.Polygon[i].y+(Segment*DeltaY/Segments) -local Tire={ -["country"]="USA", -["category"]="Fortifications", -["canCargo"]=false, -["shape_name"]="H-tyre_B_WF", -["type"]="Black_Tyre_WF", -["y"]=PointY, -["x"]=PointX, -["name"]=string.format("%s-Tire #%0d",self:GetName(),((i-1)*Segments)+Segment), -["heading"]=0, -} -local Group=coalition.addStaticObject(country.id.USA,Tire) -if UnBound and UnBound==true then -Group:destroy() -end -end -j=i -i=i+1 -end -return self -end -function ZONE_POLYGON_BASE:SmokeZone(SmokeColor) -self:F2(SmokeColor) -local i -local j -local Segments=10 -i=1 -j=#self._.Polygon -while i<=#self._.Polygon do -self:T({i,j,self._.Polygon[i],self._.Polygon[j]}) -local DeltaX=self._.Polygon[j].x-self._.Polygon[i].x -local DeltaY=self._.Polygon[j].y-self._.Polygon[i].y -for Segment=0,Segments do -local PointX=self._.Polygon[i].x+(Segment*DeltaX/Segments) -local PointY=self._.Polygon[i].y+(Segment*DeltaY/Segments) -POINT_VEC2:New(PointX,PointY):Smoke(SmokeColor) -end -j=i -i=i+1 -end -return self -end -function ZONE_POLYGON_BASE:IsVec2InZone(Vec2) -self:F2(Vec2) -local Next -local Prev -local InPolygon=false -Next=1 -Prev=#self._.Polygon -while Next<=#self._.Polygon do -self:T({Next,Prev,self._.Polygon[Next],self._.Polygon[Prev]}) -if(((self._.Polygon[Next].y>Vec2.y)~=(self._.Polygon[Prev].y>Vec2.y))and -(Vec2.x<(self._.Polygon[Prev].x-self._.Polygon[Next].x)*(Vec2.y-self._.Polygon[Next].y)/(self._.Polygon[Prev].y-self._.Polygon[Next].y)+self._.Polygon[Next].x) -)then -InPolygon=not InPolygon -end -self:T2({InPolygon=InPolygon}) -Prev=Next -Next=Next+1 -end -self:T({InPolygon=InPolygon}) -return InPolygon -end -function ZONE_POLYGON_BASE:GetRandomVec2() -self:F2() -local Vec2Found=false -local Vec2 -local BS=self:GetBoundingSquare() -self:T2(BS) -while Vec2Found==false do -Vec2={x=math.random(BS.x1,BS.x2),y=math.random(BS.y1,BS.y2)} -self:T2(Vec2) -if self:IsVec2InZone(Vec2)then -Vec2Found=true -end -end -self:T2(Vec2) -return Vec2 -end -function ZONE_POLYGON_BASE:GetRandomPointVec2() -self:F2() -local PointVec2=POINT_VEC2:NewFromVec2(self:GetRandomVec2()) -self:T2(PointVec2) -return PointVec2 -end -function ZONE_POLYGON_BASE:GetRandomPointVec3() -self:F2() -local PointVec3=POINT_VEC3:NewFromVec2(self:GetRandomVec2()) -self:T2(PointVec3) -return PointVec3 -end -function ZONE_POLYGON_BASE:GetRandomCoordinate() -self:F2() -local Coordinate=COORDINATE:NewFromVec2(self:GetRandomVec2()) -self:T2(Coordinate) -return Coordinate -end -function ZONE_POLYGON_BASE:GetBoundingSquare() -local x1=self._.Polygon[1].x -local y1=self._.Polygon[1].y -local x2=self._.Polygon[1].x -local y2=self._.Polygon[1].y -for i=2,#self._.Polygon do -self:T2({self._.Polygon[i],x1,y1,x2,y2}) -x1=(x1>self._.Polygon[i].x)and self._.Polygon[i].x or x1 -x2=(x2self._.Polygon[i].y)and self._.Polygon[i].y or y1 -y2=(y20))then -for group_num,Template in pairs(obj_type_data.group)do -if obj_type_name~="static"and Template and Template.units and type(Template.units)=='table'then -self:_RegisterGroupTemplate( -Template, -CoalitionSide, -_DATABASECategory[string.lower(CategoryName)], -CountryID -) -else -self:_RegisterStaticTemplate( -Template, -CoalitionSide, -_DATABASECategory[string.lower(CategoryName)], -CountryID -) -end -end -end -end -end -end -end -end -end -end -for ZoneID,ZoneData in pairs(env.mission.triggers.zones)do -local ZoneName=ZoneData.name -self.ZONENAMES[ZoneName]=ZoneName -end -return self -end -SET_BASE={ -ClassName="SET_BASE", -Filter={}, -Set={}, -List={}, -Index={}, -} -function SET_BASE:New(Database) -local self=BASE:Inherit(self,BASE:New()) -self.Database=Database -self.YieldInterval=10 -self.TimeInterval=0.001 -self.Set={} -self.Index={} -self.CallScheduler=SCHEDULER:New(self) -self:SetEventPriority(2) -return self -end -function SET_BASE:_Find(ObjectName) -local ObjectFound=self.Set[ObjectName] -return ObjectFound -end -function SET_BASE:GetSet() -self:F2() -return self.Set -end -function SET_BASE:Add(ObjectName,Object) -self:F(ObjectName) -self.Set[ObjectName]=Object -table.insert(self.Index,ObjectName) -end -function SET_BASE:AddObject(Object) -self:F2(Object.ObjectName) -self:T(Object.UnitName) -self:T(Object.ObjectName) -self:Add(Object.ObjectName,Object) -end -function SET_BASE:Remove(ObjectName) -local Object=self.Set[ObjectName] -self:F3({ObjectName,Object}) -if Object then -for Index,Key in ipairs(self.Index)do -if Key==ObjectName then -table.remove(self.Index,Index) -self.Set[ObjectName]=nil -break -end -end -end -end -function SET_BASE:Get(ObjectName) -self:F(ObjectName) -local Object=self.Set[ObjectName] -self:T3({ObjectName,Object}) -return Object -end -function SET_BASE:GetFirst() -local ObjectName=self.Index[1] -local FirstObject=self.Set[ObjectName] -self:T3({FirstObject}) -return FirstObject -end -function SET_BASE:GetLast() -local ObjectName=self.Index[#self.Index] -local LastObject=self.Set[ObjectName] -self:T3({LastObject}) -return LastObject -end -function SET_BASE:GetRandom() -local RandomItem=self.Set[self.Index[math.random(#self.Index)]] -self:T3({RandomItem}) -return RandomItem -end -function SET_BASE:Count() -return self.Index and#self.Index or 0 -end -function SET_BASE:SetDatabase(BaseSet) -local OtherFilter=routines.utils.deepCopy(BaseSet.Filter) -self.Filter=OtherFilter -self.Database=BaseSet:GetSet() -return self -end -function SET_BASE:SetIteratorIntervals(YieldInterval,TimeInterval) -self.YieldInterval=YieldInterval -self.TimeInterval=TimeInterval -return self -end -function SET_BASE:FilterOnce() -for ObjectName,Object in pairs(self.Database)do -if self:IsIncludeObject(Object)then -self:Add(ObjectName,Object) -end -end -return self -end -function SET_BASE:_FilterStart() -for ObjectName,Object in pairs(self.Database)do -if self:IsIncludeObject(Object)then -self:E({"Adding Object:",ObjectName}) -self:Add(ObjectName,Object) -end -end -self:HandleEvent(EVENTS.Birth,self._EventOnBirth) -self:HandleEvent(EVENTS.Dead,self._EventOnDeadOrCrash) -self:HandleEvent(EVENTS.Crash,self._EventOnDeadOrCrash) -self:HandleEvent(EVENTS.PlayerEnterUnit,self._EventOnPlayerEnterUnit) -self:HandleEvent(EVENTS.PlayerLeaveUnit,self._EventOnPlayerLeaveUnit) -return self -end -function SET_BASE:FilterDeads() -self:HandleEvent(EVENTS.Dead,self._EventOnDeadOrCrash) -return self -end -function SET_BASE:FilterCrashes() -self:HandleEvent(EVENTS.Crash,self._EventOnDeadOrCrash) -return self -end -function SET_BASE:FilterStop() -self:UnHandleEvent(EVENTS.Birth) -self:UnHandleEvent(EVENTS.Dead) -self:UnHandleEvent(EVENTS.Crash) -return self -end -function SET_BASE:FindNearestObjectFromPointVec2(PointVec2) -self:F2(PointVec2) -local NearestObject=nil -local ClosestDistance=nil -for ObjectID,ObjectData in pairs(self.Set)do -if NearestObject==nil then -NearestObject=ObjectData -ClosestDistance=PointVec2:DistanceFromVec2(ObjectData:GetVec2()) -else -local Distance=PointVec2:DistanceFromVec2(ObjectData:GetVec2()) -if DistanceMaxThreatLevelA2G then -MaxThreatLevelA2G=ThreatLevelA2G -MaxThreatText=ThreatText -end -end -self:F({MaxThreatLevelA2G=MaxThreatLevelA2G,MaxThreatText=MaxThreatText}) -return MaxThreatLevelA2G,MaxThreatText -end -function SET_UNIT:GetCoordinate() -local Coordinate=self:GetFirst():GetCoordinate() -local x1=Coordinate.x -local x2=Coordinate.x -local y1=Coordinate.y -local y2=Coordinate.y -local z1=Coordinate.z -local z2=Coordinate.z -local MaxVelocity=0 -local AvgHeading=nil -local MovingCount=0 -for UnitName,UnitData in pairs(self:GetSet())do -local Unit=UnitData -local Coordinate=Unit:GetCoordinate() -x1=(Coordinate.xx2)and Coordinate.x or x2 -y1=(Coordinate.yy2)and Coordinate.y or y2 -z1=(Coordinate.yz2)and Coordinate.z or z2 -local Velocity=Coordinate:GetVelocity() -if Velocity~=0 then -MaxVelocity=(MaxVelocity5 then -HeadingSet=nil -break -end -end -end -end -return HeadingSet -end -function SET_UNIT:HasRadar(RadarType) -self:F2(RadarType) -local RadarCount=0 -for UnitID,UnitData in pairs(self:GetSet())do -local UnitSensorTest=UnitData -local HasSensors -if RadarType then -HasSensors=UnitSensorTest:HasSensors(Unit.SensorType.RADAR,RadarType) -else -HasSensors=UnitSensorTest:HasSensors(Unit.SensorType.RADAR) -end -self:T3(HasSensors) -if HasSensors then -RadarCount=RadarCount+1 -end -end -return RadarCount -end -function SET_UNIT:HasSEAD() -self:F2() -local SEADCount=0 -for UnitID,UnitData in pairs(self:GetSet())do -local UnitSEAD=UnitData -if UnitSEAD:IsAlive()then -local UnitSEADAttributes=UnitSEAD:GetDesc().attributes -local HasSEAD=UnitSEAD:HasSEAD() -self:T3(HasSEAD) -if HasSEAD then -SEADCount=SEADCount+1 -end -end -end -return SEADCount -end -function SET_UNIT:HasGroundUnits() -self:F2() -local GroundUnitCount=0 -for UnitID,UnitData in pairs(self:GetSet())do -local UnitTest=UnitData -if UnitTest:IsGround()then -GroundUnitCount=GroundUnitCount+1 -end -end -return GroundUnitCount -end -function SET_UNIT:HasFriendlyUnits(FriendlyCoalition) -self:F2() -local FriendlyUnitCount=0 -for UnitID,UnitData in pairs(self:GetSet())do -local UnitTest=UnitData -if UnitTest:IsFriendly(FriendlyCoalition)then -FriendlyUnitCount=FriendlyUnitCount+1 -end -end -return FriendlyUnitCount -end -function SET_UNIT:IsIncludeObject(MUnit) -self:F2(MUnit) -local MUnitInclude=true -if self.Filter.Coalitions then -local MUnitCoalition=false -for CoalitionID,CoalitionName in pairs(self.Filter.Coalitions)do -self:T3({"Coalition:",MUnit:GetCoalition(),self.FilterMeta.Coalitions[CoalitionName],CoalitionName}) -if self.FilterMeta.Coalitions[CoalitionName]and self.FilterMeta.Coalitions[CoalitionName]==MUnit:GetCoalition()then -MUnitCoalition=true -end -end -MUnitInclude=MUnitInclude and MUnitCoalition -end -if self.Filter.Categories then -local MUnitCategory=false -for CategoryID,CategoryName in pairs(self.Filter.Categories)do -self:T3({"Category:",MUnit:GetDesc().category,self.FilterMeta.Categories[CategoryName],CategoryName}) -if self.FilterMeta.Categories[CategoryName]and self.FilterMeta.Categories[CategoryName]==MUnit:GetDesc().category then -MUnitCategory=true -end -end -MUnitInclude=MUnitInclude and MUnitCategory -end -if self.Filter.Types then -local MUnitType=false -for TypeID,TypeName in pairs(self.Filter.Types)do -self:T3({"Type:",MUnit:GetTypeName(),TypeName}) -if TypeName==MUnit:GetTypeName()then -MUnitType=true -end -end -MUnitInclude=MUnitInclude and MUnitType -end -if self.Filter.Countries then -local MUnitCountry=false -for CountryID,CountryName in pairs(self.Filter.Countries)do -self:T3({"Country:",MUnit:GetCountry(),CountryName}) -if country.id[CountryName]==MUnit:GetCountry()then -MUnitCountry=true -end -end -MUnitInclude=MUnitInclude and MUnitCountry -end -if self.Filter.UnitPrefixes then -local MUnitPrefix=false -for UnitPrefixId,UnitPrefix in pairs(self.Filter.UnitPrefixes)do -self:T3({"Prefix:",string.find(MUnit:GetName(),UnitPrefix,1),UnitPrefix}) -if string.find(MUnit:GetName(),UnitPrefix,1)then -MUnitPrefix=true -end -end -MUnitInclude=MUnitInclude and MUnitPrefix -end -if self.Filter.RadarTypes then -local MUnitRadar=false -for RadarTypeID,RadarType in pairs(self.Filter.RadarTypes)do -self:T3({"Radar:",RadarType}) -if MUnit:HasSensors(Unit.SensorType.RADAR,RadarType)==true then -if MUnit:GetRadar()==true then -self:T3("RADAR Found") -end -MUnitRadar=true -end -end -MUnitInclude=MUnitInclude and MUnitRadar -end -if self.Filter.SEAD then -local MUnitSEAD=false -if MUnit:HasSEAD()==true then -self:T3("SEAD Found") -MUnitSEAD=true -end -MUnitInclude=MUnitInclude and MUnitSEAD -end -self:T2(MUnitInclude) -return MUnitInclude -end -function SET_UNIT:GetTypeNames(Delimiter) -Delimiter=Delimiter or", " -local TypeReport=REPORT:New() -local Types={} -for UnitName,UnitData in pairs(self:GetSet())do -local Unit=UnitData -local UnitTypeName=Unit:GetTypeName() -if not Types[UnitTypeName]then -Types[UnitTypeName]=UnitTypeName -TypeReport:Add(UnitTypeName) -end -end -return TypeReport:Text(Delimiter) -end -SET_CLIENT={ -ClassName="SET_CLIENT", -Clients={}, -Filter={ -Coalitions=nil, -Categories=nil, -Types=nil, -Countries=nil, -ClientPrefixes=nil, -}, -FilterMeta={ -Coalitions={ -red=coalition.side.RED, -blue=coalition.side.BLUE, -neutral=coalition.side.NEUTRAL, -}, -Categories={ -plane=Unit.Category.AIRPLANE, -helicopter=Unit.Category.HELICOPTER, -ground=Unit.Category.GROUND_UNIT, -ship=Unit.Category.SHIP, -structure=Unit.Category.STRUCTURE, -}, -}, -} -function SET_CLIENT:New() -local self=BASE:Inherit(self,SET_BASE:New(_DATABASE.CLIENTS)) -return self -end -function SET_CLIENT:AddClientsByName(AddClientNames) -local AddClientNamesArray=(type(AddClientNames)=="table")and AddClientNames or{AddClientNames} -for AddClientID,AddClientName in pairs(AddClientNamesArray)do -self:Add(AddClientName,CLIENT:FindByName(AddClientName)) -end -return self -end -function SET_CLIENT:RemoveClientsByName(RemoveClientNames) -local RemoveClientNamesArray=(type(RemoveClientNames)=="table")and RemoveClientNames or{RemoveClientNames} -for RemoveClientID,RemoveClientName in pairs(RemoveClientNamesArray)do -self:Remove(RemoveClientName.ClientName) -end -return self -end -function SET_CLIENT:FindClient(ClientName) -local ClientFound=self.Set[ClientName] -return ClientFound -end -function SET_CLIENT:FilterCoalitions(Coalitions) -if not self.Filter.Coalitions then -self.Filter.Coalitions={} -end -if type(Coalitions)~="table"then -Coalitions={Coalitions} -end -for CoalitionID,Coalition in pairs(Coalitions)do -self.Filter.Coalitions[Coalition]=Coalition -end -return self -end -function SET_CLIENT:FilterCategories(Categories) -if not self.Filter.Categories then -self.Filter.Categories={} -end -if type(Categories)~="table"then -Categories={Categories} -end -for CategoryID,Category in pairs(Categories)do -self.Filter.Categories[Category]=Category -end -return self -end -function SET_CLIENT:FilterTypes(Types) -if not self.Filter.Types then -self.Filter.Types={} -end -if type(Types)~="table"then -Types={Types} -end -for TypeID,Type in pairs(Types)do -self.Filter.Types[Type]=Type -end -return self -end -function SET_CLIENT:FilterCountries(Countries) -if not self.Filter.Countries then -self.Filter.Countries={} -end -if type(Countries)~="table"then -Countries={Countries} -end -for CountryID,Country in pairs(Countries)do -self.Filter.Countries[Country]=Country -end -return self -end -function SET_CLIENT:FilterPrefixes(Prefixes) -if not self.Filter.ClientPrefixes then -self.Filter.ClientPrefixes={} -end -if type(Prefixes)~="table"then -Prefixes={Prefixes} -end -for PrefixID,Prefix in pairs(Prefixes)do -self.Filter.ClientPrefixes[Prefix]=Prefix -end -return self -end -function SET_CLIENT:FilterStart() -if _DATABASE then -self:_FilterStart() -end -return self -end -function SET_CLIENT:AddInDatabase(Event) -self:F3({Event}) -return Event.IniDCSUnitName,self.Database[Event.IniDCSUnitName] -end -function SET_CLIENT:FindInDatabase(Event) -self:F3({Event}) -return Event.IniDCSUnitName,self.Database[Event.IniDCSUnitName] -end -function SET_CLIENT:ForEachClient(IteratorFunction,...) -self:F2(arg) -self:ForEach(IteratorFunction,arg,self.Set) -return self -end -function SET_CLIENT:ForEachClientInZone(ZoneObject,IteratorFunction,...) -self:F2(arg) -self:ForEach(IteratorFunction,arg,self.Set, -function(ZoneObject,ClientObject) -if ClientObject:IsInZone(ZoneObject)then -return true -else -return false -end -end,{ZoneObject}) -return self -end -function SET_CLIENT:ForEachClientNotInZone(ZoneObject,IteratorFunction,...) -self:F2(arg) -self:ForEach(IteratorFunction,arg,self.Set, -function(ZoneObject,ClientObject) -if ClientObject:IsNotInZone(ZoneObject)then -return true -else -return false -end -end,{ZoneObject}) -return self -end -function SET_CLIENT:IsIncludeObject(MClient) -self:F2(MClient) -local MClientInclude=true -if MClient then -local MClientName=MClient.UnitName -if self.Filter.Coalitions then -local MClientCoalition=false -for CoalitionID,CoalitionName in pairs(self.Filter.Coalitions)do -local ClientCoalitionID=_DATABASE:GetCoalitionFromClientTemplate(MClientName) -self:T3({"Coalition:",ClientCoalitionID,self.FilterMeta.Coalitions[CoalitionName],CoalitionName}) -if self.FilterMeta.Coalitions[CoalitionName]and self.FilterMeta.Coalitions[CoalitionName]==ClientCoalitionID then -MClientCoalition=true -end -end -self:T({"Evaluated Coalition",MClientCoalition}) -MClientInclude=MClientInclude and MClientCoalition -end -if self.Filter.Categories then -local MClientCategory=false -for CategoryID,CategoryName in pairs(self.Filter.Categories)do -local ClientCategoryID=_DATABASE:GetCategoryFromClientTemplate(MClientName) -self:T3({"Category:",ClientCategoryID,self.FilterMeta.Categories[CategoryName],CategoryName}) -if self.FilterMeta.Categories[CategoryName]and self.FilterMeta.Categories[CategoryName]==ClientCategoryID then -MClientCategory=true -end -end -self:T({"Evaluated Category",MClientCategory}) -MClientInclude=MClientInclude and MClientCategory -end -if self.Filter.Types then -local MClientType=false -for TypeID,TypeName in pairs(self.Filter.Types)do -self:T3({"Type:",MClient:GetTypeName(),TypeName}) -if TypeName==MClient:GetTypeName()then -MClientType=true -end -end -self:T({"Evaluated Type",MClientType}) -MClientInclude=MClientInclude and MClientType -end -if self.Filter.Countries then -local MClientCountry=false -for CountryID,CountryName in pairs(self.Filter.Countries)do -local ClientCountryID=_DATABASE:GetCountryFromClientTemplate(MClientName) -self:T3({"Country:",ClientCountryID,country.id[CountryName],CountryName}) -if country.id[CountryName]and country.id[CountryName]==ClientCountryID then -MClientCountry=true -end -end -self:T({"Evaluated Country",MClientCountry}) -MClientInclude=MClientInclude and MClientCountry -end -if self.Filter.ClientPrefixes then -local MClientPrefix=false -for ClientPrefixId,ClientPrefix in pairs(self.Filter.ClientPrefixes)do -self:T3({"Prefix:",string.find(MClient.UnitName,ClientPrefix,1),ClientPrefix}) -if string.find(MClient.UnitName,ClientPrefix,1)then -MClientPrefix=true -end -end -self:T({"Evaluated Prefix",MClientPrefix}) -MClientInclude=MClientInclude and MClientPrefix -end -end -self:T2(MClientInclude) -return MClientInclude -end -SET_AIRBASE={ -ClassName="SET_AIRBASE", -Airbases={}, -Filter={ -Coalitions=nil, -}, -FilterMeta={ -Coalitions={ -red=coalition.side.RED, -blue=coalition.side.BLUE, -neutral=coalition.side.NEUTRAL, -}, -Categories={ -airdrome=Airbase.Category.AIRDROME, -helipad=Airbase.Category.HELIPAD, -ship=Airbase.Category.SHIP, -}, -}, -} -function SET_AIRBASE:New() -local self=BASE:Inherit(self,SET_BASE:New(_DATABASE.AIRBASES)) -return self -end -function SET_AIRBASE:AddAirbasesByName(AddAirbaseNames) -local AddAirbaseNamesArray=(type(AddAirbaseNames)=="table")and AddAirbaseNames or{AddAirbaseNames} -for AddAirbaseID,AddAirbaseName in pairs(AddAirbaseNamesArray)do -self:Add(AddAirbaseName,AIRBASE:FindByName(AddAirbaseName)) -end -return self -end -function SET_AIRBASE:RemoveAirbasesByName(RemoveAirbaseNames) -local RemoveAirbaseNamesArray=(type(RemoveAirbaseNames)=="table")and RemoveAirbaseNames or{RemoveAirbaseNames} -for RemoveAirbaseID,RemoveAirbaseName in pairs(RemoveAirbaseNamesArray)do -self:Remove(RemoveAirbaseName.AirbaseName) -end -return self -end -function SET_AIRBASE:FindAirbase(AirbaseName) -local AirbaseFound=self.Set[AirbaseName] -return AirbaseFound -end -function SET_AIRBASE:FilterCoalitions(Coalitions) -if not self.Filter.Coalitions then -self.Filter.Coalitions={} -end -if type(Coalitions)~="table"then -Coalitions={Coalitions} -end -for CoalitionID,Coalition in pairs(Coalitions)do -self.Filter.Coalitions[Coalition]=Coalition -end -return self -end -function SET_AIRBASE:FilterCategories(Categories) -if not self.Filter.Categories then -self.Filter.Categories={} -end -if type(Categories)~="table"then -Categories={Categories} -end -for CategoryID,Category in pairs(Categories)do -self.Filter.Categories[Category]=Category -end -return self -end -function SET_AIRBASE:FilterStart() -if _DATABASE then -self:_FilterStart() -end -return self -end -function SET_AIRBASE:AddInDatabase(Event) -self:F3({Event}) -return Event.IniDCSUnitName,self.Database[Event.IniDCSUnitName] -end -function SET_AIRBASE:FindInDatabase(Event) -self:F3({Event}) -return Event.IniDCSUnitName,self.Database[Event.IniDCSUnitName] -end -function SET_AIRBASE:ForEachAirbase(IteratorFunction,...) -self:F2(arg) -self:ForEach(IteratorFunction,arg,self.Set) -return self -end -function SET_AIRBASE:FindNearestAirbaseFromPointVec2(PointVec2) -self:F2(PointVec2) -local NearestAirbase=self:FindNearestObjectFromPointVec2(PointVec2) -return NearestAirbase -end -function SET_AIRBASE:IsIncludeObject(MAirbase) -self:F2(MAirbase) -local MAirbaseInclude=true -if MAirbase then -local MAirbaseName=MAirbase:GetName() -if self.Filter.Coalitions then -local MAirbaseCoalition=false -for CoalitionID,CoalitionName in pairs(self.Filter.Coalitions)do -local AirbaseCoalitionID=_DATABASE:GetCoalitionFromAirbase(MAirbaseName) -self:T3({"Coalition:",AirbaseCoalitionID,self.FilterMeta.Coalitions[CoalitionName],CoalitionName}) -if self.FilterMeta.Coalitions[CoalitionName]and self.FilterMeta.Coalitions[CoalitionName]==AirbaseCoalitionID then -MAirbaseCoalition=true -end -end -self:T({"Evaluated Coalition",MAirbaseCoalition}) -MAirbaseInclude=MAirbaseInclude and MAirbaseCoalition -end -if self.Filter.Categories then -local MAirbaseCategory=false -for CategoryID,CategoryName in pairs(self.Filter.Categories)do -local AirbaseCategoryID=_DATABASE:GetCategoryFromAirbase(MAirbaseName) -self:T3({"Category:",AirbaseCategoryID,self.FilterMeta.Categories[CategoryName],CategoryName}) -if self.FilterMeta.Categories[CategoryName]and self.FilterMeta.Categories[CategoryName]==AirbaseCategoryID then -MAirbaseCategory=true -end -end -self:T({"Evaluated Category",MAirbaseCategory}) -MAirbaseInclude=MAirbaseInclude and MAirbaseCategory -end -end -self:T2(MAirbaseInclude) -return MAirbaseInclude -end -SET_CARGO={ -ClassName="SET_CARGO", -Cargos={}, -Filter={ -Coalitions=nil, -Types=nil, -Countries=nil, -ClientPrefixes=nil, -}, -FilterMeta={ -Coalitions={ -red=coalition.side.RED, -blue=coalition.side.BLUE, -neutral=coalition.side.NEUTRAL, -}, -}, -} -function SET_CARGO:New() -local self=BASE:Inherit(self,SET_BASE:New(_DATABASE.CARGOS)) -return self -end -function SET_CARGO:AddCargosByName(AddCargoNames) -local AddCargoNamesArray=(type(AddCargoNames)=="table")and AddCargoNames or{AddCargoNames} -for AddCargoID,AddCargoName in pairs(AddCargoNamesArray)do -self:Add(AddCargoName,CARGO:FindByName(AddCargoName)) -end -return self -end -function SET_CARGO:RemoveCargosByName(RemoveCargoNames) -local RemoveCargoNamesArray=(type(RemoveCargoNames)=="table")and RemoveCargoNames or{RemoveCargoNames} -for RemoveCargoID,RemoveCargoName in pairs(RemoveCargoNamesArray)do -self:Remove(RemoveCargoName.CargoName) -end -return self -end -function SET_CARGO:FindCargo(CargoName) -local CargoFound=self.Set[CargoName] -return CargoFound -end -function SET_CARGO:FilterCoalitions(Coalitions) -if not self.Filter.Coalitions then -self.Filter.Coalitions={} -end -if type(Coalitions)~="table"then -Coalitions={Coalitions} -end -for CoalitionID,Coalition in pairs(Coalitions)do -self.Filter.Coalitions[Coalition]=Coalition -end -return self -end -function SET_CARGO:FilterTypes(Types) -if not self.Filter.Types then -self.Filter.Types={} -end -if type(Types)~="table"then -Types={Types} -end -for TypeID,Type in pairs(Types)do -self.Filter.Types[Type]=Type -end -return self -end -function SET_CARGO:FilterCountries(Countries) -if not self.Filter.Countries then -self.Filter.Countries={} -end -if type(Countries)~="table"then -Countries={Countries} -end -for CountryID,Country in pairs(Countries)do -self.Filter.Countries[Country]=Country -end -return self -end -function SET_CARGO:FilterPrefixes(Prefixes) -if not self.Filter.CargoPrefixes then -self.Filter.CargoPrefixes={} -end -if type(Prefixes)~="table"then -Prefixes={Prefixes} -end -for PrefixID,Prefix in pairs(Prefixes)do -self.Filter.CargoPrefixes[Prefix]=Prefix -end -return self -end -function SET_CARGO:FilterStart() -if _DATABASE then -self:_FilterStart() -end -self:HandleEvent(EVENTS.NewCargo) -self:HandleEvent(EVENTS.DeleteCargo) -return self -end -function SET_CARGO:AddInDatabase(Event) -self:F3({Event}) -return Event.IniDCSUnitName,self.Database[Event.IniDCSUnitName] -end -function SET_CARGO:FindInDatabase(Event) -self:F3({Event}) -return Event.IniDCSUnitName,self.Database[Event.IniDCSUnitName] -end -function SET_CARGO:ForEachCargo(IteratorFunction,...) -self:F2(arg) -self:ForEach(IteratorFunction,arg,self.Set) -return self -end -function SET_CARGO:FindNearestCargoFromPointVec2(PointVec2) -self:F2(PointVec2) -local NearestCargo=self:FindNearestObjectFromPointVec2(PointVec2) -return NearestCargo -end -function SET_CARGO:IsIncludeObject(MCargo) -self:F2(MCargo) -local MCargoInclude=true -if MCargo then -local MCargoName=MCargo:GetName() -if self.Filter.Coalitions then -local MCargoCoalition=false -for CoalitionID,CoalitionName in pairs(self.Filter.Coalitions)do -local CargoCoalitionID=MCargo:GetCoalition() -self:T3({"Coalition:",CargoCoalitionID,self.FilterMeta.Coalitions[CoalitionName],CoalitionName}) -if self.FilterMeta.Coalitions[CoalitionName]and self.FilterMeta.Coalitions[CoalitionName]==CargoCoalitionID then -MCargoCoalition=true -end -end -self:T({"Evaluated Coalition",MCargoCoalition}) -MCargoInclude=MCargoInclude and MCargoCoalition -end -if self.Filter.Types then -local MCargoType=false -for TypeID,TypeName in pairs(self.Filter.Types)do -self:T3({"Type:",MCargo:GetType(),TypeName}) -if TypeName==MCargo:GetType()then -MCargoType=true -end -end -self:T({"Evaluated Type",MCargoType}) -MCargoInclude=MCargoInclude and MCargoType -end -if self.Filter.CargoPrefixes then -local MCargoPrefix=false -for CargoPrefixId,CargoPrefix in pairs(self.Filter.CargoPrefixes)do -self:T3({"Prefix:",string.find(MCargo.Name,CargoPrefix,1),CargoPrefix}) -if string.find(MCargo.Name,CargoPrefix,1)then -MCargoPrefix=true -end -end -self:T({"Evaluated Prefix",MCargoPrefix}) -MCargoInclude=MCargoInclude and MCargoPrefix -end -end -self:T2(MCargoInclude) -return MCargoInclude -end -function SET_CARGO:OnEventNewCargo(EventData) -if EventData.Cargo then -if EventData.Cargo and self:IsIncludeObject(EventData.Cargo)then -self:Add(EventData.Cargo.Name,EventData.Cargo) -end -end -end -function SET_CARGO:OnEventDeleteCargo(EventData) -self:F3({EventData}) -if EventData.Cargo then -local Cargo=_DATABASE:FindCargo(EventData.Cargo.Name) -if Cargo and Cargo.Name then -self:Remove(Cargo.Name) -end -end -end -do -COORDINATE={ -ClassName="COORDINATE", -} -function COORDINATE:New(x,y,z) -local self=BASE:Inherit(self,BASE:New()) -self.x=x -self.y=y -self.z=z -return self -end -function COORDINATE:NewFromVec2(Vec2,LandHeightAdd) -local LandHeight=land.getHeight(Vec2) -LandHeightAdd=LandHeightAdd or 0 -LandHeight=LandHeight+LandHeightAdd -local self=self:New(Vec2.x,LandHeight,Vec2.y) -self:F2(self) -return self -end -function COORDINATE:NewFromVec3(Vec3) -local self=self:New(Vec3.x,Vec3.y,Vec3.z) -self:F2(self) -return self -end -function COORDINATE:GetVec3() -return{x=self.x,y=self.y,z=self.z} -end -function COORDINATE:GetVec2() -return{x=self.x,y=self.z} -end -function COORDINATE:DistanceFromVec2(Vec2Reference) -self:F2(Vec2Reference) -local Distance=((Vec2Reference.x-self.x)^2+(Vec2Reference.y-self.z)^2)^0.5 -self:T2(Distance) -return Distance -end -function COORDINATE:Translate(Distance,Angle) -local SX=self.x -local SY=self.z -local Radians=Angle/180*math.pi -local TX=Distance*math.cos(Radians)+SX -local TY=Distance*math.sin(Radians)+SY -return COORDINATE:NewFromVec2({x=TX,y=TY}) -end -function COORDINATE:GetRandomVec2InRadius(OuterRadius,InnerRadius) -self:F2({OuterRadius,InnerRadius}) -local Theta=2*math.pi*math.random() -local Radials=math.random()+math.random() -if Radials>1 then -Radials=2-Radials -end -local RadialMultiplier -if InnerRadius and InnerRadius<=OuterRadius then -RadialMultiplier=(OuterRadius-InnerRadius)*Radials+InnerRadius -else -RadialMultiplier=OuterRadius*Radials -end -local RandomVec2 -if OuterRadius>0 then -RandomVec2={x=math.cos(Theta)*RadialMultiplier+self.x,y=math.sin(Theta)*RadialMultiplier+self.z} -else -RandomVec2={x=self.x,y=self.z} -end -return RandomVec2 -end -function COORDINATE:GetRandomVec3InRadius(OuterRadius,InnerRadius) -local RandomVec2=self:GetRandomVec2InRadius(OuterRadius,InnerRadius) -local y=self.y+math.random(InnerRadius,OuterRadius) -local RandomVec3={x=RandomVec2.x,y=y,z=RandomVec2.y} -return RandomVec3 -end -function COORDINATE:GetLandHeight() -local Vec2={x=self.x,y=self.z} -return land.getHeight(Vec2) -end -function COORDINATE:SetHeading(Heading) -self.Heading=Heading -end -function COORDINATE:GetHeading() -return self.Heading -end -function COORDINATE:SetVelocity(Velocity) -self.Velocity=Velocity -end -function COORDINATE:GetVelocity() -local Velocity=self.Velocity -return Velocity or 0 -end -function COORDINATE:GetMovingText(Settings) -return self:GetVelocityText(Settings)..", "..self:GetHeadingText(Settings) -end -function COORDINATE:GetDirectionVec3(TargetCoordinate) -return{x=TargetCoordinate.x-self.x,y=TargetCoordinate.y-self.y,z=TargetCoordinate.z-self.z} -end -function COORDINATE:GetNorthCorrectionRadians() -local TargetVec3=self:GetVec3() -local lat,lon=coord.LOtoLL(TargetVec3) -local north_posit=coord.LLtoLO(lat+1,lon) -return math.atan2(north_posit.z-TargetVec3.z,north_posit.x-TargetVec3.x) -end -function COORDINATE:GetAngleRadians(DirectionVec3) -local DirectionRadians=math.atan2(DirectionVec3.z,DirectionVec3.x) -if DirectionRadians<0 then -DirectionRadians=DirectionRadians+2*math.pi -end -return DirectionRadians -end -function COORDINATE:GetAngleDegrees(DirectionVec3) -local AngleRadians=self:GetAngleRadians(DirectionVec3) -local Angle=UTILS.ToDegree(AngleRadians) -return Angle -end -function COORDINATE:Get2DDistance(TargetCoordinate) -local TargetVec3=TargetCoordinate:GetVec3() -local SourceVec3=self:GetVec3() -return((TargetVec3.x-SourceVec3.x)^2+(TargetVec3.z-SourceVec3.z)^2)^0.5 -end -function COORDINATE:Get3DDistance(TargetCoordinate) -local TargetVec3=TargetCoordinate:GetVec3() -local SourceVec3=self:GetVec3() -return((TargetVec3.x-SourceVec3.x)^2+(TargetVec3.y-SourceVec3.y)^2+(TargetVec3.z-SourceVec3.z)^2)^0.5 -end -function COORDINATE:GetBearingText(AngleRadians,Precision,Settings) -local Settings=Settings or _SETTINGS -local AngleDegrees=UTILS.Round(UTILS.ToDegree(AngleRadians),Precision) -local s=string.format('%03d°',AngleDegrees) -return s -end -function COORDINATE:GetDistanceText(Distance,Settings) -local Settings=Settings or _SETTINGS -local DistanceText -if Settings:IsMetric()then -DistanceText=" for "..UTILS.Round(Distance/1000,2).." km" -else -DistanceText=" for "..UTILS.Round(UTILS.MetersToNM(Distance),2).." miles" -end -return DistanceText -end -function COORDINATE:GetAltitudeText(Settings) -local Altitude=self.y -local Settings=Settings or _SETTINGS -if Altitude~=0 then -if Settings:IsMetric()then -return" at "..UTILS.Round(self.y,-3).." meters" -else -return" at "..UTILS.Round(UTILS.MetersToFeet(self.y),-3).." feet" -end -else -return"" -end -end -function COORDINATE:GetVelocityText(Settings) -local Velocity=self:GetVelocity() -local Settings=Settings or _SETTINGS -if Velocity then -if Settings:IsMetric()then -return string.format(" moving at %d km/h",UTILS.MpsToKmph(Velocity)) -else -return string.format(" moving at %d mi/h",UTILS.MpsToKmph(Velocity)/1.852) -end -else -return" stationary" -end -end -function COORDINATE:GetHeadingText(Settings) -local Heading=self:GetHeading() -if Heading then -return string.format(" bearing %3d°",Heading) -else -return" bearing unknown" -end -end -function COORDINATE:GetBRText(AngleRadians,Distance,Settings) -local Settings=Settings or _SETTINGS -local BearingText=self:GetBearingText(AngleRadians,0,Settings) -local DistanceText=self:GetDistanceText(Distance,Settings) -local BRText=BearingText..DistanceText -return BRText -end -function COORDINATE:GetBRAText(AngleRadians,Distance,Settings) -local Settings=Settings or _SETTINGS -local BearingText=self:GetBearingText(AngleRadians,0,Settings) -local DistanceText=self:GetDistanceText(Distance,Settings) -local AltitudeText=self:GetAltitudeText(Settings) -local BRAText=BearingText..DistanceText..AltitudeText -return BRAText -end -function COORDINATE:Translate(Distance,Angle) -local SX=self.x -local SZ=self.z -local Radians=Angle/180*math.pi -local TX=Distance*math.cos(Radians)+SX -local TZ=Distance*math.sin(Radians)+SZ -return COORDINATE:New(TX,self.y,TZ) -end -function COORDINATE:WaypointAir(AltType,Type,Action,Speed,SpeedLocked) -self:F2({AltType,Type,Action,Speed,SpeedLocked}) -local RoutePoint={} -RoutePoint.x=self.x -RoutePoint.y=self.z -RoutePoint.alt=self.y -RoutePoint.alt_type=AltType or"RADIO" -RoutePoint.type=Type or nil -RoutePoint.action=Action or nil -RoutePoint.speed=(Speed and Speed/3.6)or(500/3.6) -RoutePoint.speed_locked=true -RoutePoint.task={} -RoutePoint.task.id="ComboTask" -RoutePoint.task.params={} -RoutePoint.task.params.tasks={} -return RoutePoint -end -function COORDINATE:WaypointGround(Speed,Formation) -self:F2({Formation,Speed}) -local RoutePoint={} -RoutePoint.x=self.x -RoutePoint.y=self.z -RoutePoint.action=Formation or"" -RoutePoint.speed=(Speed or 999)/3.6 -RoutePoint.speed_locked=true -RoutePoint.task={} -RoutePoint.task.id="ComboTask" -RoutePoint.task.params={} -RoutePoint.task.params.tasks={} -return RoutePoint -end -function COORDINATE:Explosion(ExplosionIntensity) -self:F2({ExplosionIntensity}) -trigger.action.explosion(self:GetVec3(),ExplosionIntensity) -end -function COORDINATE:IlluminationBomb() -self:F2() -trigger.action.illuminationBomb(self:GetVec3()) -end -function COORDINATE:Smoke(SmokeColor) -self:F2({SmokeColor}) -trigger.action.smoke(self:GetVec3(),SmokeColor) -end -function COORDINATE:SmokeGreen() -self:F2() -self:Smoke(SMOKECOLOR.Green) -end -function COORDINATE:SmokeRed() -self:F2() -self:Smoke(SMOKECOLOR.Red) -end -function COORDINATE:SmokeWhite() -self:F2() -self:Smoke(SMOKECOLOR.White) -end -function COORDINATE:SmokeOrange() -self:F2() -self:Smoke(SMOKECOLOR.Orange) -end -function COORDINATE:SmokeBlue() -self:F2() -self:Smoke(SMOKECOLOR.Blue) -end -function COORDINATE:Flare(FlareColor,Azimuth) -self:F2({FlareColor}) -trigger.action.signalFlare(self:GetVec3(),FlareColor,Azimuth and Azimuth or 0) -end -function COORDINATE:FlareWhite(Azimuth) -self:F2(Azimuth) -self:Flare(FLARECOLOR.White,Azimuth) -end -function COORDINATE:FlareYellow(Azimuth) -self:F2(Azimuth) -self:Flare(FLARECOLOR.Yellow,Azimuth) -end -function COORDINATE:FlareGreen(Azimuth) -self:F2(Azimuth) -self:Flare(FLARECOLOR.Green,Azimuth) -end -function COORDINATE:FlareRed(Azimuth) -self:F2(Azimuth) -self:Flare(FLARECOLOR.Red,Azimuth) -end -do -function COORDINATE:MarkToAll(MarkText) -local MarkID=UTILS.GetMarkID() -trigger.action.markToAll(MarkID,MarkText,self:GetVec3()) -return MarkID -end -function COORDINATE:MarkToCoalition(MarkText,Coalition) -local MarkID=UTILS.GetMarkID() -trigger.action.markToCoalition(MarkID,MarkText,self:GetVec3(),Coalition) -return MarkID -end -function COORDINATE:MarkToCoalitionRed(MarkText) -return self:MarkToCoalition(MarkText,coalition.side.RED) -end -function COORDINATE:MarkToCoalitionBlue(MarkText) -return self:MarkToCoalition(MarkText,coalition.side.BLUE) -end -function COORDINATE:MarkToGroup(MarkText,MarkGroup) -local MarkID=UTILS.GetMarkID() -trigger.action.markToGroup(MarkID,MarkText,self:GetVec3(),MarkGroup:GetID()) -return MarkID -end -function COORDINATE:RemoveMark(MarkID) -trigger.action.removeMark(MarkID) -end -end -function COORDINATE:IsLOS(ToCoordinate) -local FromVec3=self:GetVec3() -FromVec3.y=FromVec3.y+2 -local ToVec3=ToCoordinate:GetVec3() -ToVec3.y=ToVec3.y+2 -local IsLOS=land.isVisible(FromVec3,ToVec3) -return IsLOS -end -function COORDINATE:IsInRadius(Coordinate,Radius) -local InVec2=self:GetVec2() -local Vec2=Coordinate:GetVec2() -local InRadius=UTILS.IsInRadius(InVec2,Vec2,Radius) -return InRadius -end -function COORDINATE:IsInSphere(Coordinate,Radius) -local InVec3=self:GetVec3() -local Vec3=Coordinate:GetVec3() -local InSphere=UTILS.IsInSphere(InVec3,Vec3,Radius) -return InSphere -end -function COORDINATE:ToStringBR(FromCoordinate,Settings) -local DirectionVec3=FromCoordinate:GetDirectionVec3(self) -local AngleRadians=self:GetAngleRadians(DirectionVec3) -local Distance=self:Get2DDistance(FromCoordinate) -return"BR, "..self:GetBRText(AngleRadians,Distance,Settings) -end -function COORDINATE:ToStringBRA(FromCoordinate,Settings) -local DirectionVec3=FromCoordinate:GetDirectionVec3(self) -local AngleRadians=self:GetAngleRadians(DirectionVec3) -local Distance=FromCoordinate:Get2DDistance(self) -local Altitude=self:GetAltitudeText() -return"BRA, "..self:GetBRAText(AngleRadians,Distance,Settings) -end -function COORDINATE:ToStringBULLS(Coalition,Settings) -local TargetCoordinate=COORDINATE:NewFromVec3(coalition.getMainRefPoint(Coalition)) -local DirectionVec3=self:GetDirectionVec3(TargetCoordinate) -local AngleRadians=self:GetAngleRadians(DirectionVec3) -local Distance=self:Get2DDistance(TargetCoordinate) -local Altitude=self:GetAltitudeText() -return"BULLS, "..self:GetBRText(AngleRadians,Distance,Settings) -end -function COORDINATE:ToStringAspect(TargetCoordinate) -local Heading=self.Heading -local DirectionVec3=self:GetDirectionVec3(TargetCoordinate) -local Angle=self:GetAngleDegrees(DirectionVec3) -if Heading then -local Aspect=Angle-Heading -if Aspect>-135 and Aspect<=-45 then -return"Flanking" -end -if Aspect>-45 and Aspect<=45 then -return"Hot" -end -if Aspect>45 and Aspect<=135 then -return"Flanking" -end -if Aspect>135 or Aspect<=-135 then -return"Cold" -end -end -return"" -end -function COORDINATE:ToStringLLDMS(Settings) -local LL_Accuracy=Settings and Settings.LL_Accuracy or _SETTINGS.LL_Accuracy -local lat,lon=coord.LOtoLL(self:GetVec3()) -return"LL DMS, "..UTILS.tostringLL(lat,lon,LL_Accuracy,true) -end -function COORDINATE:ToStringLLDDM(Settings) -local LL_Accuracy=Settings and Settings.LL_Accuracy or _SETTINGS.LL_Accuracy -local lat,lon=coord.LOtoLL(self:GetVec3()) -return"LL DDM, "..UTILS.tostringLL(lat,lon,LL_Accuracy,false) -end -function COORDINATE:ToStringMGRS(Settings) -local MGRS_Accuracy=Settings and Settings.MGRS_Accuracy or _SETTINGS.MGRS_Accuracy -local lat,lon=coord.LOtoLL(self:GetVec3()) -local MGRS=coord.LLtoMGRS(lat,lon) -return"MGRS, "..UTILS.tostringMGRS(MGRS,MGRS_Accuracy) -end -function COORDINATE:ToStringFromRP(ReferenceCoord,ReferenceName,Controllable,Settings) -self:E({ReferenceCoord=ReferenceCoord,ReferenceName=ReferenceName}) -local Settings=Settings or(Controllable and _DATABASE:GetPlayerSettings(Controllable:GetPlayerName()))or _SETTINGS -local IsAir=Controllable and Controllable:IsAirPlane()or false -if IsAir then -local DirectionVec3=ReferenceCoord:GetDirectionVec3(self) -local AngleRadians=self:GetAngleRadians(DirectionVec3) -local Distance=self:Get2DDistance(ReferenceCoord) -return"Targets are the last seen "..self:GetBRText(AngleRadians,Distance,Settings).." from "..ReferenceName -else -local DirectionVec3=ReferenceCoord:GetDirectionVec3(self) -local AngleRadians=self:GetAngleRadians(DirectionVec3) -local Distance=self:Get2DDistance(ReferenceCoord) -return"Target are located "..self:GetBRText(AngleRadians,Distance,Settings).." from "..ReferenceName -end -return nil -end -function COORDINATE:ToStringA2G(Controllable,Settings) -self:F({Controllable=Controllable and Controllable:GetName()}) -local Settings=Settings or(Controllable and _DATABASE:GetPlayerSettings(Controllable:GetPlayerName()))or _SETTINGS -if Settings:IsA2G_BR()then -if Controllable then -local Coordinate=Controllable:GetCoordinate() -return Controllable and self:ToStringBR(Coordinate,Settings)or self:ToStringMGRS(Settings) -else -return self:ToStringMGRS(Settings) -end -end -if Settings:IsA2G_LL_DMS()then -return self:ToStringLLDMS(Settings) -end -if Settings:IsA2G_LL_DDM()then -return self:ToStringLLDDM(Settings) -end -if Settings:IsA2G_MGRS()then -return self:ToStringMGRS(Settings) -end -return nil -end -function COORDINATE:ToStringA2A(Controllable,Settings) -self:F({Controllable=Controllable and Controllable:GetName()}) -local Settings=Settings or(Controllable and _DATABASE:GetPlayerSettings(Controllable:GetPlayerName()))or _SETTINGS -if Settings:IsA2A_BRAA()then -if Controllable then -local Coordinate=Controllable:GetCoordinate() -return self:ToStringBRA(Coordinate,Settings) -else -return self:ToStringMGRS(Settings) -end -end -if Settings:IsA2A_BULLS()then -local Coalition=Controllable:GetCoalition() -return self:ToStringBULLS(Coalition,Settings) -end -if Settings:IsA2A_LL_DMS()then -return self:ToStringLLDMS(Settings) -end -if Settings:IsA2A_LL_DDM()then -return self:ToStringLLDDM(Settings) -end -if Settings:IsA2A_MGRS()then -return self:ToStringMGRS(Settings) -end -return nil -end -function COORDINATE:ToString(Controllable,Settings,Task) -self:F({Controllable=Controllable and Controllable:GetName()}) -local Settings=Settings or(Controllable and _DATABASE:GetPlayerSettings(Controllable:GetPlayerName()))or _SETTINGS -local ModeA2A=true -if Task then -if Task:IsInstanceOf(TASK_A2A)then -ModeA2A=true -else -if Task:IsInstanceOf(TASK_A2G)then -ModeA2A=false -else -if Task:IsInstanceOf(TASK_CARGO)then -ModeA2A=false -end -end -end -else -local IsAir=Controllable and Controllable:IsAirPlane()or false -if IsAir then -ModeA2A=true -else -ModeA2A=false -end -end -if ModeA2A==true then -return self:ToStringA2A(Controllable,Settings) -else -return self:ToStringA2G(Controllable,Settings) -end -return nil -end -end -do -POINT_VEC3={ -ClassName="POINT_VEC3", -Metric=true, -RoutePointAltType={ -BARO="BARO", -}, -RoutePointType={ -TakeOffParking="TakeOffParking", -TurningPoint="Turning Point", -}, -RoutePointAction={ -FromParkingArea="From Parking Area", -TurningPoint="Turning Point", -}, -} -function POINT_VEC3:New(x,y,z) -local self=BASE:Inherit(self,COORDINATE:New(x,y,z)) -self:F2(self) -return self -end -function POINT_VEC3:NewFromVec2(Vec2,LandHeightAdd) -local self=BASE:Inherit(self,COORDINATE:NewFromVec2(Vec2,LandHeightAdd)) -self:F2(self) -return self -end -function POINT_VEC3:NewFromVec3(Vec3) -local self=BASE:Inherit(self,COORDINATE:NewFromVec3(Vec3)) -self:F2(self) -return self -end -function POINT_VEC3:GetX() -return self.x -end -function POINT_VEC3:GetY() -return self.y -end -function POINT_VEC3:GetZ() -return self.z -end -function POINT_VEC3:SetX(x) -self.x=x -return self -end -function POINT_VEC3:SetY(y) -self.y=y -return self -end -function POINT_VEC3:SetZ(z) -self.z=z -return self -end -function POINT_VEC3:AddX(x) -self.x=self.x+x -return self -end -function POINT_VEC3:AddY(y) -self.y=self.y+y -return self -end -function POINT_VEC3:AddZ(z) -self.z=self.z+z -return self -end -function POINT_VEC3:GetRandomPointVec3InRadius(OuterRadius,InnerRadius) -return POINT_VEC3:NewFromVec3(self:GetRandomVec3InRadius(OuterRadius,InnerRadius)) -end -end -do -POINT_VEC2={ -ClassName="POINT_VEC2", -} -function POINT_VEC2:New(x,y,LandHeightAdd) -local LandHeight=land.getHeight({["x"]=x,["y"]=y}) -LandHeightAdd=LandHeightAdd or 0 -LandHeight=LandHeight+LandHeightAdd -local self=BASE:Inherit(self,COORDINATE:New(x,LandHeight,y)) -self:F2(self) -return self -end -function POINT_VEC2:NewFromVec2(Vec2,LandHeightAdd) -local LandHeight=land.getHeight(Vec2) -LandHeightAdd=LandHeightAdd or 0 -LandHeight=LandHeight+LandHeightAdd -local self=BASE:Inherit(self,COORDINATE:NewFromVec2(Vec2,LandHeightAdd)) -self:F2(self) -return self -end -function POINT_VEC2:NewFromVec3(Vec3) -local self=BASE:Inherit(self,COORDINATE:NewFromVec3(Vec3)) -self:F2(self) -return self -end -function POINT_VEC2:GetX() -return self.x -end -function POINT_VEC2:GetY() -return self.z -end -function POINT_VEC2:SetX(x) -self.x=x -return self -end -function POINT_VEC2:SetY(y) -self.z=y -return self -end -function POINT_VEC2:GetLat() -return self.x -end -function POINT_VEC2:SetLat(x) -self.x=x -return self -end -function POINT_VEC2:GetLon() -return self.z -end -function POINT_VEC2:SetLon(z) -self.z=z -return self -end -function POINT_VEC2:GetAlt() -return self.y~=0 or land.getHeight({x=self.x,y=self.z}) -end -function POINT_VEC2:SetAlt(Altitude) -self.y=Altitude or land.getHeight({x=self.x,y=self.z}) -return self -end -function POINT_VEC2:AddX(x) -self.x=self.x+x -return self -end -function POINT_VEC2:AddY(y) -self.z=self.z+y -return self -end -function POINT_VEC2:AddAlt(Altitude) -self.y=land.getHeight({x=self.x,y=self.z})+Altitude or 0 -return self -end -function POINT_VEC2:GetRandomPointVec2InRadius(OuterRadius,InnerRadius) -self:F2({OuterRadius,InnerRadius}) -return POINT_VEC2:NewFromVec2(self:GetRandomVec2InRadius(OuterRadius,InnerRadius)) -end -function POINT_VEC2:DistanceFromPointVec2(PointVec2Reference) -self:F2(PointVec2Reference) -local Distance=((PointVec2Reference.x-self.x)^2+(PointVec2Reference.z-self.z)^2)^0.5 -self:T2(Distance) -return Distance -end -end -MESSAGE={ -ClassName="MESSAGE", -MessageCategory=0, -MessageID=0, -} -MESSAGE.Type={ -Update="Update", -Information="Information", -Briefing="Briefing Report", -Overview="Overview Report", -Detailed="Detailed Report" -} -function MESSAGE:New(MessageText,MessageDuration,MessageCategory) -local self=BASE:Inherit(self,BASE:New()) -self:F({MessageText,MessageDuration,MessageCategory}) -self.MessageType=nil -if MessageCategory and MessageCategory~=""then -if MessageCategory:sub(-1)~="\n"then -self.MessageCategory=MessageCategory..": " -else -self.MessageCategory=MessageCategory:sub(1,-2)..":\n" -end -else -self.MessageCategory="" -end -self.MessageDuration=MessageDuration or 5 -self.MessageTime=timer.getTime() -self.MessageText=MessageText:gsub("^\n","",1):gsub("\n$","",1) -self.MessageSent=false -self.MessageGroup=false -self.MessageCoalition=false -return self -end -function MESSAGE:NewType(MessageText,MessageType) -local self=BASE:Inherit(self,BASE:New()) -self:F({MessageText}) -self.MessageType=MessageType -self.MessageTime=timer.getTime() -self.MessageText=MessageText:gsub("^\n","",1):gsub("\n$","",1) -return self -end -function MESSAGE:ToClient(Client,Settings) -self:F(Client) -if Client and Client:GetClientGroupID()then -if self.MessageType then -local Settings=Settings or(Client and _DATABASE:GetPlayerSettings(Client:GetPlayerName()))or _SETTINGS -self.MessageDuration=Settings:GetMessageTime(self.MessageType) -self.MessageCategory=self.MessageType..": " -end -if self.MessageDuration~=0 then -local ClientGroupID=Client:GetClientGroupID() -self:T(self.MessageCategory..self.MessageText:gsub("\n$",""):gsub("\n$","").." / "..self.MessageDuration) -trigger.action.outTextForGroup(ClientGroupID,self.MessageCategory..self.MessageText:gsub("\n$",""):gsub("\n$",""),self.MessageDuration) -end -end -return self -end -function MESSAGE:ToGroup(Group,Settings) -self:F(Group.GroupName) -if Group then -if self.MessageType then -local Settings=Settings or(Group and _DATABASE:GetPlayerSettings(Group:GetPlayerName()))or _SETTINGS -self.MessageDuration=Settings:GetMessageTime(self.MessageType) -self.MessageCategory=self.MessageType..": " -end -if self.MessageDuration~=0 then -self:T(self.MessageCategory..self.MessageText:gsub("\n$",""):gsub("\n$","").." / "..self.MessageDuration) -trigger.action.outTextForGroup(Group:GetID(),self.MessageCategory..self.MessageText:gsub("\n$",""):gsub("\n$",""),self.MessageDuration) -end -end -return self -end -function MESSAGE:ToBlue() -self:F() -self:ToCoalition(coalition.side.BLUE) -return self -end -function MESSAGE:ToRed() -self:F() -self:ToCoalition(coalition.side.RED) -return self -end -function MESSAGE:ToCoalition(CoalitionSide,Settings) -self:F(CoalitionSide) -if self.MessageType then -local Settings=Settings or _SETTINGS -self.MessageDuration=Settings:GetMessageTime(self.MessageType) -self.MessageCategory=self.MessageType..": " -end -if CoalitionSide then -if self.MessageDuration~=0 then -self:T(self.MessageCategory..self.MessageText:gsub("\n$",""):gsub("\n$","").." / "..self.MessageDuration) -trigger.action.outTextForCoalition(CoalitionSide,self.MessageCategory..self.MessageText:gsub("\n$",""):gsub("\n$",""),self.MessageDuration) -end -end -return self -end -function MESSAGE:ToCoalitionIf(CoalitionSide,Condition) -self:F(CoalitionSide) -if Condition and Condition==true then -self:ToCoalition(CoalitionSide) -end -return self -end -function MESSAGE:ToAll() -self:F() -if self.MessageType then -local Settings=Settings or _SETTINGS -self.MessageDuration=Settings:GetMessageTime(self.MessageType) -self.MessageCategory=self.MessageType..": " -end -if self.MessageDuration~=0 then -self:T(self.MessageCategory..self.MessageText:gsub("\n$",""):gsub("\n$","").." / "..self.MessageDuration) -trigger.action.outText(self.MessageCategory..self.MessageText:gsub("\n$",""):gsub("\n$",""),self.MessageDuration) -end -return self -end -function MESSAGE:ToAllIf(Condition) -if Condition and Condition==true then -self:ToAll() -end -return self -end -do -FSM={ -ClassName="FSM", -} -function FSM:New(FsmT) -self=BASE:Inherit(self,BASE:New()) -self.options=options or{} -self.options.subs=self.options.subs or{} -self.current=self.options.initial or'none' -self.Events={} -self.subs={} -self.endstates={} -self.Scores={} -self._StartState="none" -self._Transitions={} -self._Processes={} -self._EndStates={} -self._Scores={} -self._EventSchedules={} -self.CallScheduler=SCHEDULER:New(self) -return self -end -function FSM:SetStartState(State) -self._StartState=State -self.current=State -end -function FSM:GetStartState() -return self._StartState or{} -end -function FSM:AddTransition(From,Event,To) -local Transition={} -Transition.From=From -Transition.Event=Event -Transition.To=To -self:T2(Transition) -self._Transitions[Transition]=Transition -self:_eventmap(self.Events,Transition) -end -function FSM:GetTransitions() -return self._Transitions or{} -end -function FSM:AddProcess(From,Event,Process,ReturnEvents) -self:T({From,Event}) -local Sub={} -Sub.From=From -Sub.Event=Event -Sub.fsm=Process -Sub.StartEvent="Start" -Sub.ReturnEvents=ReturnEvents -self._Processes[Sub]=Sub -self:_submap(self.subs,Sub,nil) -self:AddTransition(From,Event,From) -return Process -end -function FSM:GetProcesses() -return self._Processes or{} -end -function FSM:GetProcess(From,Event) -for ProcessID,Process in pairs(self:GetProcesses())do -if Process.From==From and Process.Event==Event then -return Process.fsm -end -end -error("Sub-Process from state "..From.." with event "..Event.." not found!") -end -function FSM:AddEndState(State) -self._EndStates[State]=State -self.endstates[State]=State -end -function FSM:GetEndStates() -return self._EndStates or{} -end -function FSM:AddScore(State,ScoreText,Score) -self:F({State,ScoreText,Score}) -self._Scores[State]=self._Scores[State]or{} -self._Scores[State].ScoreText=ScoreText -self._Scores[State].Score=Score -return self -end -function FSM:AddScoreProcess(From,Event,State,ScoreText,Score) -self:F({From,Event,State,ScoreText,Score}) -local Process=self:GetProcess(From,Event) -Process._Scores[State]=Process._Scores[State]or{} -Process._Scores[State].ScoreText=ScoreText -Process._Scores[State].Score=Score -self:T(Process._Scores) -return Process -end -function FSM:GetScores() -return self._Scores or{} -end -function FSM:GetSubs() -return self.options.subs -end -function FSM:LoadCallBacks(CallBackTable) -for name,callback in pairs(CallBackTable or{})do -self[name]=callback -end -end -function FSM:_eventmap(Events,EventStructure) -local Event=EventStructure.Event -local __Event="__"..EventStructure.Event -self[Event]=self[Event]or self:_create_transition(Event) -self[__Event]=self[__Event]or self:_delayed_transition(Event) -self:T2("Added methods: "..Event..", "..__Event) -Events[Event]=self.Events[Event]or{map={}} -self:_add_to_map(Events[Event].map,EventStructure) -end -function FSM:_submap(subs,sub,name) -subs[sub.From]=subs[sub.From]or{} -subs[sub.From][sub.Event]=subs[sub.From][sub.Event]or{} -subs[sub.From][sub.Event][sub]={} -subs[sub.From][sub.Event][sub].fsm=sub.fsm -subs[sub.From][sub.Event][sub].StartEvent=sub.StartEvent -subs[sub.From][sub.Event][sub].ReturnEvents=sub.ReturnEvents or{} -subs[sub.From][sub.Event][sub].name=name -subs[sub.From][sub.Event][sub].fsmparent=self -end -function FSM:_call_handler(handler,params,EventName) -local ErrorHandler=function(errmsg) -env.info("Error in SCHEDULER function:"..errmsg) -if debug~=nil then -env.info(debug.traceback()) -end -return errmsg -end -if self[handler]then -self:T2("Calling "..handler) -self._EventSchedules[EventName]=nil -local Result,Value=xpcall(function()return self[handler](self,unpack(params))end,ErrorHandler) -return Value -end -end -function FSM._handler(self,EventName,...) -local Can,to=self:can(EventName) -if to=="*"then -to=self.current -end -if Can then -local from=self.current -local params={from,EventName,to,...} -if self.Controllable then -self:T("FSM Transition for "..self.Controllable.ControllableName.." :"..self.current.." --> "..EventName.." --> "..to) -else -self:T("FSM Transition:"..self.current.." --> "..EventName.." --> "..to) -end -if(self:_call_handler("onbefore"..EventName,params,EventName)==false) -or(self:_call_handler("OnBefore"..EventName,params,EventName)==false) -or(self:_call_handler("onleave"..from,params,EventName)==false) -or(self:_call_handler("OnLeave"..from,params,EventName)==false)then -self:T("Cancel Transition") -return false -end -self.current=to -local execute=true -local subtable=self:_gosub(from,EventName) -for _,sub in pairs(subtable)do -self:T("calling sub start event: "..sub.StartEvent) -sub.fsm.fsmparent=self -sub.fsm.ReturnEvents=sub.ReturnEvents -sub.fsm[sub.StartEvent](sub.fsm) -execute=false -end -local fsmparent,Event=self:_isendstate(to) -if fsmparent and Event then -self:F2({"end state: ",fsmparent,Event}) -self:_call_handler("onenter"..to,params,EventName) -self:_call_handler("OnEnter"..to,params,EventName) -self:_call_handler("onafter"..EventName,params,EventName) -self:_call_handler("OnAfter"..EventName,params,EventName) -self:_call_handler("onstatechange",params,EventName) -fsmparent[Event](fsmparent) -execute=false -end -if execute then -self:_call_handler("onenter"..to,params,EventName) -self:_call_handler("OnEnter"..to,params,EventName) -self:_call_handler("onafter"..EventName,params,EventName) -self:_call_handler("OnAfter"..EventName,params,EventName) -self:_call_handler("onstatechange",params,EventName) -end -else -self:T("Cannot execute transition.") -self:T({From=self.current,Event=EventName,To=to,Can=Can}) -end -return nil -end -function FSM:_delayed_transition(EventName) -return function(self,DelaySeconds,...) -self:T2("Delayed Event: "..EventName) -local CallID=0 -if DelaySeconds~=nil then -if DelaySeconds<0 then -DelaySeconds=math.abs(DelaySeconds) -if not self._EventSchedules[EventName]then -CallID=self.CallScheduler:Schedule(self,self._handler,{EventName,...},DelaySeconds or 1) -self._EventSchedules[EventName]=CallID -else -end -else -CallID=self.CallScheduler:Schedule(self,self._handler,{EventName,...},DelaySeconds or 1) -end -else -error("FSM: An asynchronous event trigger requires a DelaySeconds parameter!!! This can be positive or negative! Sorry, but will not process this.") -end -self:T2({CallID=CallID}) -end -end -function FSM:_create_transition(EventName) -return function(self,...)return self._handler(self,EventName,...)end -end -function FSM:_gosub(ParentFrom,ParentEvent) -local fsmtable={} -if self.subs[ParentFrom]and self.subs[ParentFrom][ParentEvent]then -self:T({ParentFrom,ParentEvent,self.subs[ParentFrom],self.subs[ParentFrom][ParentEvent]}) -return self.subs[ParentFrom][ParentEvent] -else -return{} -end -end -function FSM:_isendstate(Current) -local FSMParent=self.fsmparent -if FSMParent and self.endstates[Current]then -self:T({state=Current,endstates=self.endstates,endstate=self.endstates[Current]}) -FSMParent.current=Current -local ParentFrom=FSMParent.current -self:T(ParentFrom) -self:T(self.ReturnEvents) -local Event=self.ReturnEvents[Current] -self:T({ParentFrom,Event,self.ReturnEvents}) -if Event then -return FSMParent,Event -else -self:T({"Could not find parent event name for state ",ParentFrom}) -end -end -return nil -end -function FSM:_add_to_map(Map,Event) -self:F3({Map,Event}) -if type(Event.From)=='string'then -Map[Event.From]=Event.To -else -for _,From in ipairs(Event.From)do -Map[From]=Event.To -end -end -self:T3({Map,Event}) -end -function FSM:GetState() -return self.current -end -function FSM:Is(State) -return self.current==State -end -function FSM:is(state) -return self.current==state -end -function FSM:can(e) -local Event=self.Events[e] -self:F3({self.current,Event}) -local To=Event and Event.map[self.current]or Event.map['*'] -return To~=nil,To -end -function FSM:cannot(e) -return not self:can(e) -end -end -do -FSM_CONTROLLABLE={ -ClassName="FSM_CONTROLLABLE", -} -function FSM_CONTROLLABLE:New(FSMT,Controllable) -local self=BASE:Inherit(self,FSM:New(FSMT)) -if Controllable then -self:SetControllable(Controllable) -end -self:AddTransition("*","Stop","Stopped") -return self -end -function FSM_CONTROLLABLE:OnAfterStop(Controllable,From,Event,To) -self.CallScheduler:Clear() -end -function FSM_CONTROLLABLE:SetControllable(FSMControllable) -self.Controllable=FSMControllable -end -function FSM_CONTROLLABLE:GetControllable() -return self.Controllable -end -function FSM_CONTROLLABLE:_call_handler(handler,params,EventName) -local ErrorHandler=function(errmsg) -env.info("Error in SCHEDULER function:"..errmsg) -if debug~=nil then -env.info(debug.traceback()) -end -return errmsg -end -if self[handler]then -self:F3("Calling "..handler) -self._EventSchedules[EventName]=nil -local Result,Value=xpcall(function()return self[handler](self,self.Controllable,unpack(params))end,ErrorHandler) -return Value -end -end -end -do -FSM_PROCESS={ -ClassName="FSM_PROCESS", -} -function FSM_PROCESS:New(Controllable,Task) -local self=BASE:Inherit(self,FSM_CONTROLLABLE:New()) -self:Assign(Controllable,Task) -return self -end -function FSM_PROCESS:Init(FsmProcess) -self:T("No Initialisation") -end -function FSM_PROCESS:_call_handler(handler,params,EventName) -local ErrorHandler=function(errmsg) -env.info("Error in FSM_PROCESS call handler:"..errmsg) -if debug~=nil then -env.info(debug.traceback()) -end -return errmsg -end -if self[handler]then -self:F3("Calling "..handler) -self._EventSchedules[EventName]=nil -local Result,Value=xpcall(function()return self[handler](self,self.Controllable,self.Task,unpack(params))end,ErrorHandler) -return Value -end -end -function FSM_PROCESS:Copy(Controllable,Task) -self:T({self:GetClassNameAndID()}) -local NewFsm=self:New(Controllable,Task) -NewFsm:Assign(Controllable,Task) -NewFsm:Init(self) -NewFsm:SetStartState(self:GetStartState()) -for TransitionID,Transition in pairs(self:GetTransitions())do -NewFsm:AddTransition(Transition.From,Transition.Event,Transition.To) -end -for ProcessID,Process in pairs(self:GetProcesses())do -local FsmProcess=NewFsm:AddProcess(Process.From,Process.Event,Process.fsm:Copy(Controllable,Task),Process.ReturnEvents) -end -for EndStateID,EndState in pairs(self:GetEndStates())do -self:T(EndState) -NewFsm:AddEndState(EndState) -end -for ScoreID,Score in pairs(self:GetScores())do -self:T(Score) -NewFsm:AddScore(ScoreID,Score.ScoreText,Score.Score) -end -return NewFsm -end -function FSM_PROCESS:Remove() -self:F({self:GetClassNameAndID()}) -self:F("Clearing Schedules") -self.CallScheduler:Clear() -for ProcessID,Process in pairs(self:GetProcesses())do -if Process.fsm then -Process.fsm:Remove() -Process.fsm=nil -end -end -return self -end -function FSM_PROCESS:SetTask(Task) -self.Task=Task -return self -end -function FSM_PROCESS:GetTask() -return self.Task -end -function FSM_PROCESS:GetMission() -return self.Task.Mission -end -function FSM_PROCESS:GetCommandCenter() -return self:GetTask():GetMission():GetCommandCenter() -end -function FSM_PROCESS:Message(Message) -self:F({Message=Message}) -local CC=self:GetCommandCenter() -local TaskGroup=self.Controllable:GetGroup() -local PlayerName=self.Controllable:GetPlayerName() -PlayerName=PlayerName and" ("..PlayerName..")"or"" -local Callsign=self.Controllable:GetCallsign() -local Prefix=Callsign and" @ "..Callsign..PlayerName or"" -Message=Prefix..": "..Message -CC:MessageToGroup(Message,TaskGroup) -end -function FSM_PROCESS:Assign(ProcessUnit,Task) -self:SetControllable(ProcessUnit) -self:SetTask(Task) -return self -end -function FSM_PROCESS:onenterAssigned(ProcessUnit) -self:T("Assign") -self.Task:Assign() -end -function FSM_PROCESS:onenterFailed(ProcessUnit) -self:T("Failed") -self.Task:Fail() -end -function FSM_PROCESS:onstatechange(ProcessUnit,Task,From,Event,To,Dummy) -self:T({ProcessUnit:GetName(),From,Event,To,Dummy,self:IsTrace()}) -if self:IsTrace()then -end -self:T({Scores=self._Scores,To=To}) -if self._Scores[To]then -local Task=self.Task -local Scoring=Task:GetScoring() -if Scoring then -Scoring:_AddMissionTaskScore(Task.Mission,ProcessUnit,self._Scores[To].ScoreText,self._Scores[To].Score) -end -end -end -end -do -FSM_TASK={ -ClassName="FSM_TASK", -} -function FSM_TASK:New(FSMT) -local self=BASE:Inherit(self,FSM_CONTROLLABLE:New(FSMT)) -self["onstatechange"]=self.OnStateChange -return self -end -function FSM_TASK:_call_handler(handler,params,EventName) -if self[handler]then -self:T("Calling "..handler) -self._EventSchedules[EventName]=nil -return self[handler](self,unpack(params)) -end -end -end -do -FSM_SET={ -ClassName="FSM_SET", -} -function FSM_SET:New(FSMSet) -self=BASE:Inherit(self,FSM:New()) -if FSMSet then -self:Set(FSMSet) -end -return self -end -function FSM_SET:Set(FSMSet) -self:F(FSMSet) -self.Set=FSMSet -end -function FSM_SET:Get() -return self.Controllable -end -function FSM_SET:_call_handler(handler,params,EventName) -if self[handler]then -self:T("Calling "..handler) -self._EventSchedules[EventName]=nil -return self[handler](self,self.Set,unpack(params)) -end -end -end -RADIO={ -ClassName="RADIO", -FileName="", -Frequency=0, -Modulation=radio.modulation.AM, -Subtitle="", -SubtitleDuration=0, -Power=100, -Loop=true, -} -function RADIO:New(Positionable) -local self=BASE:Inherit(self,BASE:New()) -self.Loop=true -self:F(Positionable) -if Positionable:GetPointVec2()then -self.Positionable=Positionable -return self -end -self:E({"The passed positionable is invalid, no RADIO created",Positionable}) -return nil -end -function RADIO:SetFileName(FileName) -self:F2(FileName) -if type(FileName)=="string"then -if FileName:find(".ogg")or FileName:find(".wav")then -if not FileName:find("l10n/DEFAULT/")then -FileName="l10n/DEFAULT/"..FileName -end -self.FileName=FileName -return self -end -end -self:E({"File name invalid. Maybe something wrong with the extension ?",self.FileName}) -return self -end -function RADIO:SetFrequency(Frequency) -self:F2(Frequency) -if type(Frequency)=="number"then -if(Frequency>=30 and Frequency<88)or(Frequency>=108 and Frequency<152)or(Frequency>=225 and Frequency<400)then -self.Frequency=Frequency*1000000 -if self.Positionable.ClassName=="UNIT"or self.Positionable.ClassName=="GROUP"then -self.Positionable:SetCommand({ -id="SetFrequency", -params={ -frequency=self.Frequency, -modulation=self.Modulation, -} -}) -end -return self -end -end -self:E({"Frequency is outside of DCS Frequency ranges (30-80, 108-152, 225-400). Frequency unchanged.",self.Frequency}) -return self -end -function RADIO:SetModulation(Modulation) -self:F2(Modulation) -if type(Modulation)=="number"then -if Modulation==radio.modulation.AM or Modulation==radio.modulation.FM then -self.Modulation=Modulation -return self -end -end -self:E({"Modulation is invalid. Use DCS's enum radio.modulation. Modulation unchanged.",self.Modulation}) -return self -end -function RADIO:SetPower(Power) -self:F2(Power) -if type(Power)=="number"then -self.Power=math.floor(math.abs(Power)) -return self -end -self:E({"Power is invalid. Power unchanged.",self.Power}) -return self -end -function RADIO:SetLoop(Loop) -self:F2(Loop) -if type(Loop)=="boolean"then -self.Loop=Loop -return self -end -self:E({"Loop is invalid. Loop unchanged.",self.Loop}) -return self -end -function RADIO:SetSubtitle(Subtitle,SubtitleDuration) -self:F2({Subtitle,SubtitleDuration}) -if type(Subtitle)=="string"then -self.Subtitle=Subtitle -else -self.Subtitle="" -self:E({"Subtitle is invalid. Subtitle reset.",self.Subtitle}) -end -if type(SubtitleDuration)=="number"then -if math.floor(math.abs(SubtitleDuration))==SubtitleDuration then -self.SubtitleDuration=SubtitleDuration -return self -end -end -self.SubtitleDuration=0 -self:E({"SubtitleDuration is invalid. SubtitleDuration reset.",self.SubtitleDuration}) -end -function RADIO:NewGenericTransmission(FileName,Frequency,Modulation,Power,Loop) -self:F({FileName,Frequency,Modulation,Power}) -self:SetFileName(FileName) -if Frequency then self:SetFrequency(Frequency)end -if Modulation then self:SetModulation(Modulation)end -if Power then self:SetPower(Power)end -if Loop then self:SetLoop(Loop)end -return self -end -function RADIO:NewUnitTransmission(FileName,Subtitle,SubtitleDuration,Frequency,Modulation,Loop) -self:F({FileName,Subtitle,SubtitleDuration,Frequency,Modulation,Loop}) -self:SetFileName(FileName) -if Subtitle then self:SetSubtitle(Subtitle)end -if SubtitleDuration then self:SetSubtitleDuration(SubtitleDuration)end -if Frequency then self:SetFrequency(Frequency)end -if Modulation then self:SetModulation(Modulation)end -if Loop then self:SetLoop(Loop)end -return self -end -function RADIO:Broadcast() -self:F() -if self.Positionable.ClassName=="UNIT"or self.Positionable.ClassName=="GROUP"then -self:T2("Broadcasting from a UNIT or a GROUP") -self.Positionable:SetCommand({ -id="TransmitMessage", -params={ -file=self.FileName, -duration=self.SubtitleDuration, -subtitle=self.Subtitle, -loop=self.Loop, -} -}) -else -self:T2("Broadcasting from a POSITIONABLE") -trigger.action.radioTransmission(self.FileName,self.Positionable:GetPositionVec3(),self.Modulation,self.Loop,self.Frequency,self.Power,tostring(self.ID)) -end -return self -end -function RADIO:StopBroadcast() -self:F() -if self.Positionable.ClassName=="UNIT"or self.Positionable.ClassName=="GROUP"then -self.Positionable:SetCommand({ -id="StopTransmission", -params={} -}) -else -trigger.action.stopRadioTransmission(tostring(self.ID)) -end -return self -end -BEACON={ -ClassName="BEACON", -} -function BEACON:New(Positionable) -local self=BASE:Inherit(self,BASE:New()) -self:F(Positionable) -if Positionable:GetPointVec2()then -self.Positionable=Positionable -return self -end -self:E({"The passed positionable is invalid, no BEACON created",Positionable}) -return nil -end -function BEACON:_TACANToFrequency(TACANChannel,TACANMode) -self:F3({TACANChannel,TACANMode}) -if type(TACANChannel)~="number"then -if TACANMode~="X"and TACANMode~="Y"then -return nil -end -end -local A=1151 -local B=64 -if TACANChannel<64 then -B=1 -end -if TACANMode=='Y'then -A=1025 -if TACANChannel<64 then -A=1088 -end -else -if TACANChannel<64 then -A=962 -end -end -return(A+TACANChannel-B)*1000000 -end -function BEACON:AATACAN(TACANChannel,Message,Bearing,BeaconDuration) -self:F({TACANChannel,Message,Bearing,BeaconDuration}) -local IsValid=true -if not self.Positionable:IsAir()then -self:E({"The POSITIONABLE you want to attach the AA Tacan Beacon is not an aircraft ! The BEACON is not emitting",self.Positionable}) -IsValid=false -end -local Frequency=self:_TACANToFrequency(TACANChannel,"Y") -if not Frequency then -self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) -IsValid=false -end -local System -if Bearing then -System=5 -else -System=14 -end -if IsValid then -self:T2({"AA TACAN BEACON started !"}) -self.Positionable:SetCommand({ -id="ActivateBeacon", -params={ -type=4, -system=System, -callsign=Message, -frequency=Frequency, -} -}) -if BeaconDuration then -SCHEDULER:New(nil, -function() -self:StopAATACAN() -end,{},BeaconDuration) -end -end -return self -end -function BEACON:StopAATACAN() -self:F() -if not self.Positionable then -self:E({"Start the beacon first before stoping it !"}) -else -self.Positionable:SetCommand({ -id='DeactivateBeacon', -params={ -} -}) -end -end -function BEACON:RadioBeacon(FileName,Frequency,Modulation,Power,BeaconDuration) -self:F({FileName,Frequency,Modulation,Power,BeaconDuration}) -local IsValid=false -if type(FileName)=="string"then -if FileName:find(".ogg")or FileName:find(".wav")then -if not FileName:find("l10n/DEFAULT/")then -FileName="l10n/DEFAULT/"..FileName -end -IsValid=true -end -end -if not IsValid then -self:E({"File name invalid. Maybe something wrong with the extension ? ",FileName}) -end -if type(Frequency)~="number"and IsValid then -self:E({"Frequency invalid. ",Frequency}) -IsValid=false -end -Frequency=Frequency*1000000 -if Modulation~=radio.modulation.AM and Modulation~=radio.modulation.FM and IsValid then -self:E({"Modulation is invalid. Use DCS's enum radio.modulation.",Modulation}) -IsValid=false -end -if type(Power)~="number"and IsValid then -self:E({"Power is invalid. ",Power}) -IsValid=false -end -Power=math.floor(math.abs(Power)) -if IsValid then -self:T2({"Activating Beacon on ",Frequency,Modulation}) -trigger.action.radioTransmission(FileName,self.Positionable:GetPositionVec3(),Modulation,true,Frequency,Power,tostring(self.ID)) -if BeaconDuration then -SCHEDULER:New(nil, -function() -self:StopRadioBeacon() -end,{},BeaconDuration) -end -end -end -function BEACON:StopRadioBeacon() -self:F() -trigger.action.stopRadioTransmission(tostring(self.ID)) -end -SPAWNSTATIC={ -ClassName="SPAWNSTATIC", -} -function SPAWNSTATIC:NewFromStatic(SpawnTemplatePrefix,CountryID) -local self=BASE:Inherit(self,BASE:New()) -self:F({SpawnTemplatePrefix}) -local TemplateStatic=StaticObject.getByName(SpawnTemplatePrefix) -if TemplateStatic then -self.SpawnTemplatePrefix=SpawnTemplatePrefix -self.CountryID=CountryID -self.SpawnIndex=0 -else -error("SPAWNSTATIC:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '"..SpawnTemplatePrefix.."'") -end -self:SetEventPriority(5) -return self -end -function SPAWNSTATIC:NewFromType(SpawnTypeName,SpawnShapeName,SpawnCategory,CountryID) -local self=BASE:Inherit(self,BASE:New()) -self:F({SpawnTypeName}) -self.SpawnTypeName=SpawnTypeName -self.CountryID=CountryID -self.SpawnIndex=0 -self:SetEventPriority(5) -return self -end -function SPAWNSTATIC:SpawnFromPointVec2(PointVec2,Heading,NewName) -self:F({PointVec2,Heading,NewName}) -local CountryName=_DATABASE.COUNTRY_NAME[self.CountryID] -local StaticTemplate=_DATABASE:GetStaticUnitTemplate(self.SpawnTemplatePrefix) -StaticTemplate.x=PointVec2:GetLat() -StaticTemplate.y=PointVec2:GetLon() -StaticTemplate.name=NewName or string.format("%s#%05d",self.SpawnTemplatePrefix,self.SpawnIndex) -StaticTemplate.heading=(Heading/180)*math.pi -StaticTemplate.CountryID=nil -StaticTemplate.CoalitionID=nil -StaticTemplate.CategoryID=nil -local Static=coalition.addStaticObject(self.CountryID,StaticTemplate) -self.SpawnIndex=self.SpawnIndex+1 -return Static -end -function SPAWNSTATIC:SpawnFromZone(Zone,Heading,NewName) -self:F({Zone,Heading,NewName}) -local Static=self:SpawnFromPointVec2(Zone:GetPointVec2(),Heading,NewName) -return Static -end -CARGOS={} -do -CARGO={ -ClassName="CARGO", -Type=nil, -Name=nil, -Weight=nil, -CargoObject=nil, -CargoCarrier=nil, -Representable=false, -Slingloadable=false, -Moveable=false, -Containable=false, -} -function CARGO:New(Type,Name,Weight) -local self=BASE:Inherit(self,FSM:New()) -self:F({Type,Name,Weight}) -self:SetStartState("UnLoaded") -self:AddTransition({"UnLoaded","Boarding"},"Board","Boarding") -self:AddTransition("Boarding","Boarding","Boarding") -self:AddTransition("Boarding","CancelBoarding","UnLoaded") -self:AddTransition("Boarding","Load","Loaded") -self:AddTransition("UnLoaded","Load","Loaded") -self:AddTransition("Loaded","UnBoard","UnBoarding") -self:AddTransition("UnBoarding","UnBoarding","UnBoarding") -self:AddTransition("UnBoarding","UnLoad","UnLoaded") -self:AddTransition("Loaded","UnLoad","UnLoaded") -self:AddTransition("*","Damaged","Damaged") -self:AddTransition("*","Destroyed","Destroyed") -self:AddTransition("*","Respawn","UnLoaded") -self.Type=Type -self.Name=Name -self.Weight=Weight -self.CargoObject=nil -self.CargoCarrier=nil -self.Representable=false -self.Slingloadable=false -self.Moveable=false -self.Containable=false -self:SetDeployed(false) -self.CargoScheduler=SCHEDULER:New() -CARGOS[self.Name]=self -return self -end -function CARGO:Destroy() -if self.CargoObject then -self.CargoObject:Destroy() -end -self:Destroyed() -end -function CARGO:GetName() -return self.Name -end -function CARGO:GetObjectName() -if self:IsLoaded()then -return self.CargoCarrier:GetName() -else -return self.CargoObject:GetName() -end -end -function CARGO:GetType() -return self.Type -end -function CARGO:GetCoordinate() -return self.CargoObject:GetCoordinate() -end -function CARGO:IsDestroyed() -return self:Is("Destroyed") -end -function CARGO:IsLoaded() -return self:Is("Loaded") -end -function CARGO:IsUnLoaded() -return self:Is("UnLoaded") -end -function CARGO:IsAlive() -if self:IsLoaded()then -return self.CargoCarrier:IsAlive() -else -return self.CargoObject:IsAlive() -end -end -function CARGO:SetDeployed(Deployed) -self.Deployed=Deployed -end -function CARGO:IsDeployed() -return self.Deployed -end -function CARGO:Spawn(PointVec2) -self:F() -end -function CARGO:Flare(FlareColor) -if self:IsUnLoaded()then -trigger.action.signalFlare(self.CargoObject:GetVec3(),FlareColor,0) -end -end -function CARGO:FlareWhite() -self:Flare(trigger.flareColor.White) -end -function CARGO:FlareYellow() -self:Flare(trigger.flareColor.Yellow) -end -function CARGO:FlareGreen() -self:Flare(trigger.flareColor.Green) -end -function CARGO:FlareRed() -self:Flare(trigger.flareColor.Red) -end -function CARGO:Smoke(SmokeColor,Range) -self:F2() -if self:IsUnLoaded()then -if Range then -trigger.action.smoke(self.CargoObject:GetRandomVec3(Range),SmokeColor) -else -trigger.action.smoke(self.CargoObject:GetVec3(),SmokeColor) -end -end -end -function CARGO:SmokeGreen() -self:Smoke(trigger.smokeColor.Green,Range) -end -function CARGO:SmokeRed() -self:Smoke(trigger.smokeColor.Red,Range) -end -function CARGO:SmokeWhite() -self:Smoke(trigger.smokeColor.White,Range) -end -function CARGO:SmokeOrange() -self:Smoke(trigger.smokeColor.Orange,Range) -end -function CARGO:SmokeBlue() -self:Smoke(trigger.smokeColor.Blue,Range) -end -function CARGO:IsInZone(Zone) -self:F({Zone}) -if self:IsLoaded()then -return Zone:IsPointVec2InZone(self.CargoCarrier:GetPointVec2()) -else -self:F({Size=self.CargoObject:GetSize(),Units=self.CargoObject:GetUnits()}) -if self.CargoObject:GetSize()~=0 then -return Zone:IsPointVec2InZone(self.CargoObject:GetPointVec2()) -else -return false -end -end -return nil -end -function CARGO:IsNear(PointVec2,NearRadius) -self:F({PointVec2,NearRadius}) -local Distance=PointVec2:DistanceFromPointVec2(self.CargoObject:GetPointVec2()) -self:T(Distance) -if Distance<=NearRadius then -return true -else -return false -end -end -function CARGO:GetPointVec2() -return self.CargoObject:GetPointVec2() -end -function CARGO:GetCoordinate() -return self.CargoObject:GetCoordinate() -end -function CARGO:SetWeight(Weight) -self.Weight=Weight -return self -end -end -do -CARGO_REPRESENTABLE={ -ClassName="CARGO_REPRESENTABLE" -} -function CARGO_REPRESENTABLE:New(CargoObject,Type,Name,Weight,ReportRadius,NearRadius) -local self=BASE:Inherit(self,CARGO:New(Type,Name,Weight,ReportRadius,NearRadius)) -self:F({Type,Name,Weight,ReportRadius,NearRadius}) -return self -end -function CARGO_REPRESENTABLE:RouteTo(ToPointVec2,Speed) -self:F2(ToPointVec2) -local Points={} -local PointStartVec2=self.CargoObject:GetPointVec2() -Points[#Points+1]=PointStartVec2:WaypointGround(Speed) -Points[#Points+1]=ToPointVec2:WaypointGround(Speed) -local TaskRoute=self.CargoObject:TaskRoute(Points) -self.CargoObject:SetTask(TaskRoute,2) -return self -end -end -do -CARGO_REPORTABLE={ -ClassName="CARGO_REPORTABLE" -} -function CARGO_REPORTABLE:New(CargoObject,Type,Name,Weight,ReportRadius) -local self=BASE:Inherit(self,CARGO:New(Type,Name,Weight)) -self:F({Type,Name,Weight,ReportRadius}) -self.CargoSet=SET_CARGO:New() -self.ReportRadius=ReportRadius or 1000 -self.CargoObject=CargoObject -return self -end -function CARGO_REPORTABLE:IsInRadius(PointVec2) -self:F({PointVec2}) -local Distance=0 -if self:IsLoaded()then -Distance=PointVec2:DistanceFromPointVec2(self.CargoCarrier:GetPointVec2()) -else -Distance=PointVec2:DistanceFromPointVec2(self.CargoObject:GetPointVec2()) -end -self:T(Distance) -if Distance<=self.ReportRadius then -return true -else -return false -end -end -function CARGO_REPORTABLE:MessageToGroup(Message,TaskGroup,Name) -local Prefix=Name and"@ "..Name..": "or"@ "..TaskGroup:GetCallsign()..": " -Message=Prefix..Message -MESSAGE:New(Message,20,"Cargo: "..self:GetName()):ToGroup(TaskGroup) -end -function CARGO_REPORTABLE:GetBoardingRange() -return self.ReportRadius -end -function CARGO_REPORTABLE:Respawn() -self:F({"Respawning"}) -for CargoID,CargoData in pairs(self.CargoSet:GetSet())do -local Cargo=CargoData -Cargo:Destroy() -Cargo:SetStartState("UnLoaded") -end -local CargoObject=self.CargoObject -CargoObject:Destroy() -local Template=CargoObject:GetTemplate() -CargoObject:Respawn(Template) -self:SetDeployed(false) -local WeightGroup=0 -self:SetStartState("UnLoaded") -end -end -do -CARGO_UNIT={ -ClassName="CARGO_UNIT" -} -function CARGO_UNIT:New(CargoUnit,Type,Name,Weight,NearRadius) -local self=BASE:Inherit(self,CARGO_REPRESENTABLE:New(CargoUnit,Type,Name,Weight,NearRadius)) -self:F({Type,Name,Weight,NearRadius}) -self:T(CargoUnit) -self.CargoObject=CargoUnit -self:T(self.ClassName) -self:SetEventPriority(5) -return self -end -function CARGO_UNIT:Destroy() -self:F({CargoName=self:GetName()}) -_EVENTDISPATCHER:CreateEventDeleteCargo(self) -return self -end -function CARGO_UNIT:onenterUnBoarding(From,Event,To,ToPointVec2,NearRadius) -self:F({From,Event,To,ToPointVec2,NearRadius}) -NearRadius=NearRadius or 25 -local Angle=180 -local Speed=60 -local DeployDistance=9 -local RouteDistance=60 -if From=="Loaded"then -local CargoCarrier=self.CargoCarrier -local CargoCarrierPointVec2=CargoCarrier:GetPointVec2() -local CargoCarrierHeading=self.CargoCarrier:GetHeading() -local CargoDeployHeading=((CargoCarrierHeading+Angle)>=360)and(CargoCarrierHeading+Angle-360)or(CargoCarrierHeading+Angle) -local CargoRoutePointVec2=CargoCarrierPointVec2:Translate(RouteDistance,CargoDeployHeading) -ToPointVec2=ToPointVec2 or CargoRoutePointVec2 -local DirectionVec3=CargoCarrierPointVec2:GetDirectionVec3(ToPointVec2) -local Angle=CargoCarrierPointVec2:GetAngleDegrees(DirectionVec3) -local CargoDeployPointVec2=CargoCarrierPointVec2:Translate(DeployDistance,Angle) -local FromPointVec2=CargoCarrierPointVec2 -if self.CargoObject then -self.CargoObject:ReSpawn(CargoDeployPointVec2:GetVec3(),CargoDeployHeading) -self:F({"CargoUnits:",self.CargoObject:GetGroup():GetName()}) -self.CargoCarrier=nil -local Points={} -Points[#Points+1]=CargoCarrierPointVec2:WaypointGround(Speed) -Points[#Points+1]=ToPointVec2:WaypointGround(Speed) -local TaskRoute=self.CargoObject:TaskRoute(Points) -self.CargoObject:SetTask(TaskRoute,1) -self:__UnBoarding(1,ToPointVec2,NearRadius) -end -end -end -function CARGO_UNIT:onleaveUnBoarding(From,Event,To,ToPointVec2,NearRadius) -self:F({From,Event,To,ToPointVec2,NearRadius}) -NearRadius=NearRadius or 25 -local Angle=180 -local Speed=10 -local Distance=5 -if From=="UnBoarding"then -if self:IsNear(ToPointVec2,NearRadius)then -return true -else -self:__UnBoarding(1,ToPointVec2,NearRadius) -end -return false -end -end -function CARGO_UNIT:onafterUnBoarding(From,Event,To,ToPointVec2,NearRadius) -self:F({From,Event,To,ToPointVec2,NearRadius}) -NearRadius=NearRadius or 25 -self.CargoInAir=self.CargoObject:InAir() -self:T(self.CargoInAir) -if not self.CargoInAir then -end -self:__UnLoad(1,ToPointVec2,NearRadius) -end -function CARGO_UNIT:onenterUnLoaded(From,Event,To,ToPointVec2) -self:F({ToPointVec2,From,Event,To}) -local Angle=180 -local Speed=10 -local Distance=5 -if From=="Loaded"then -local StartPointVec2=self.CargoCarrier:GetPointVec2() -local CargoCarrierHeading=self.CargoCarrier:GetHeading() -local CargoDeployHeading=((CargoCarrierHeading+Angle)>=360)and(CargoCarrierHeading+Angle-360)or(CargoCarrierHeading+Angle) -local CargoDeployPointVec2=StartPointVec2:Translate(Distance,CargoDeployHeading) -ToPointVec2=ToPointVec2 or POINT_VEC2:New(CargoDeployPointVec2:GetX(),CargoDeployPointVec2:GetY()) -if self.CargoObject then -self.CargoObject:ReSpawn(ToPointVec2:GetVec3(),0) -self.CargoCarrier=nil -end -end -if self.OnUnLoadedCallBack then -self.OnUnLoadedCallBack(self,unpack(self.OnUnLoadedParameters)) -self.OnUnLoadedCallBack=nil -end -end -function CARGO_UNIT:onafterBoard(From,Event,To,CargoCarrier,NearRadius,...) -self:F({From,Event,To,CargoCarrier,NearRadius}) -local NearRadius=NearRadius or 25 -self.CargoInAir=self.CargoObject:InAir() -self:T(self.CargoInAir) -if not self.CargoInAir then -if self:IsNear(CargoCarrier:GetPointVec2(),NearRadius)then -self:Load(CargoCarrier,NearRadius,...) -else -local Speed=90 -local Angle=180 -local Distance=5 -NearRadius=NearRadius or 25 -local CargoCarrierPointVec2=CargoCarrier:GetPointVec2() -local CargoCarrierHeading=CargoCarrier:GetHeading() -local CargoDeployHeading=((CargoCarrierHeading+Angle)>=360)and(CargoCarrierHeading+Angle-360)or(CargoCarrierHeading+Angle) -local CargoDeployPointVec2=CargoCarrierPointVec2:Translate(Distance,CargoDeployHeading) -local Points={} -local PointStartVec2=self.CargoObject:GetPointVec2() -Points[#Points+1]=PointStartVec2:WaypointGround(Speed) -Points[#Points+1]=CargoDeployPointVec2:WaypointGround(Speed) -local TaskRoute=self.CargoObject:TaskRoute(Points) -self.CargoObject:SetTask(TaskRoute,2) -self:__Boarding(-1,CargoCarrier,NearRadius) -self.RunCount=0 -end -end -end -function CARGO_UNIT:onafterBoarding(From,Event,To,CargoCarrier,NearRadius,...) -self:F({From,Event,To,CargoCarrier.UnitName,NearRadius}) -if CargoCarrier and CargoCarrier:IsAlive()then -if CargoCarrier:InAir()==false then -if self:IsNear(CargoCarrier:GetPointVec2(),NearRadius)then -self:__Load(1,CargoCarrier,...) -else -self:__Boarding(-1,CargoCarrier,NearRadius,...) -self.RunCount=self.RunCount+1 -if self.RunCount>=20 then -self.RunCount=0 -local Speed=90 -local Angle=180 -local Distance=5 -NearRadius=NearRadius or 25 -local CargoCarrierPointVec2=CargoCarrier:GetPointVec2() -local CargoCarrierHeading=CargoCarrier:GetHeading() -local CargoDeployHeading=((CargoCarrierHeading+Angle)>=360)and(CargoCarrierHeading+Angle-360)or(CargoCarrierHeading+Angle) -local CargoDeployPointVec2=CargoCarrierPointVec2:Translate(Distance,CargoDeployHeading) -local Points={} -local PointStartVec2=self.CargoObject:GetPointVec2() -Points[#Points+1]=PointStartVec2:WaypointGround(Speed) -Points[#Points+1]=CargoDeployPointVec2:WaypointGround(Speed) -local TaskRoute=self.CargoObject:TaskRoute(Points) -self.CargoObject:SetTask(TaskRoute,0.2) -end -end -else -self.CargoObject:MessageToGroup("Cancelling Boarding... Get back on the ground!",5,CargoCarrier:GetGroup(),self:GetName()) -self:CancelBoarding(CargoCarrier,NearRadius,...) -self.CargoObject:SetCommand(self.CargoObject:CommandStopRoute(true)) -end -else -self:E("Something is wrong") -end -end -function CARGO_UNIT:onenterBoarding(From,Event,To,CargoCarrier,NearRadius,...) -self:F({From,Event,To,CargoCarrier.UnitName,NearRadius}) -local Speed=90 -local Angle=180 -local Distance=5 -local NearRadius=NearRadius or 25 -if From=="UnLoaded"or From=="Boarding"then -end -end -function CARGO_UNIT:onenterLoaded(From,Event,To,CargoCarrier) -self:F({From,Event,To,CargoCarrier}) -self.CargoCarrier=CargoCarrier -if self.CargoObject then -self:T("Destroying") -self.CargoObject:Destroy() -end -end -end -do -CARGO_GROUP={ -ClassName="CARGO_GROUP", -} -function CARGO_GROUP:New(CargoGroup,Type,Name,ReportRadius) -local self=BASE:Inherit(self,CARGO_REPORTABLE:New(CargoGroup,Type,Name,0,ReportRadius)) -self:F({Type,Name,ReportRadius}) -self.CargoObject=CargoGroup -self:SetDeployed(false) -self.CargoGroup=CargoGroup -local WeightGroup=0 -for UnitID,UnitData in pairs(CargoGroup:GetUnits())do -local Unit=UnitData -local WeightUnit=Unit:GetDesc().massEmpty -WeightGroup=WeightGroup+WeightUnit -local CargoUnit=CARGO_UNIT:New(Unit,Type,Unit:GetName(),WeightUnit) -self.CargoSet:Add(CargoUnit:GetName(),CargoUnit) -end -self:SetWeight(WeightGroup) -self:T({"Weight Cargo",WeightGroup}) -_EVENTDISPATCHER:CreateEventNewCargo(self) -self:HandleEvent(EVENTS.Dead,self.OnEventCargoDead) -self:HandleEvent(EVENTS.Crash,self.OnEventCargoDead) -self:HandleEvent(EVENTS.PlayerLeaveUnit,self.OnEventCargoDead) -self:SetEventPriority(4) -return self -end -function CARGO_GROUP:OnEventCargoDead(EventData) -local Destroyed=false -if self:IsDestroyed()or self:IsUnLoaded()then -Destroyed=true -for CargoID,CargoData in pairs(self.CargoSet:GetSet())do -local Cargo=CargoData -if Cargo:IsAlive()then -Destroyed=false -else -Cargo:Destroyed() -end -end -else -local CarrierName=self.CargoCarrier:GetName() -if CarrierName==EventData.IniDCSUnitName then -MESSAGE:New("Cargo is lost from carrier "..CarrierName,15):ToAll() -Destroyed=true -self.CargoCarrier:ClearCargo() -end -end -if Destroyed then -self:Destroyed() -self:E({"Cargo group destroyed"}) -end -end -function CARGO_GROUP:onenterBoarding(From,Event,To,CargoCarrier,NearRadius,...) -self:F({CargoCarrier.UnitName,From,Event,To}) -local NearRadius=NearRadius or 25 -if From=="UnLoaded"then -self.CargoSet:ForEach( -function(Cargo,...) -Cargo:__Board(1,CargoCarrier,NearRadius,...) -end,... -) -self:__Boarding(1,CargoCarrier,NearRadius,...) -end -end -function CARGO_GROUP:onenterLoaded(From,Event,To,CargoCarrier,...) -self:F({From,Event,To,CargoCarrier,...}) -if From=="UnLoaded"then -for CargoID,Cargo in pairs(self.CargoSet:GetSet())do -Cargo:Load(CargoCarrier) -end -end -self.CargoCarrier=CargoCarrier -end -function CARGO_GROUP:onafterBoarding(From,Event,To,CargoCarrier,NearRadius,...) -self:F({CargoCarrier.UnitName,From,Event,To}) -local NearRadius=NearRadius or 25 -local Boarded=true -local Cancelled=false -local Dead=true -self.CargoSet:Flush() -for CargoID,Cargo in pairs(self.CargoSet:GetSet())do -self:T({Cargo:GetName(),Cargo.current}) -if not Cargo:is("Loaded")then -Boarded=false -end -if Cargo:is("UnLoaded")then -Cancelled=true -end -if not Cargo:is("Destroyed")then -Dead=false -end -end -if not Dead then -if not Cancelled then -if not Boarded then -self:__Boarding(1,CargoCarrier,NearRadius,...) -else -self:__Load(1,CargoCarrier,...) -end -else -self:__CancelBoarding(1,CargoCarrier,NearRadius,...) -end -else -self:__Destroyed(1,CargoCarrier,NearRadius,...) -end -end -function CARGO_GROUP:GetCount() -return self.CargoSet:Count() -end -function CARGO_GROUP:onenterUnBoarding(From,Event,To,ToPointVec2,NearRadius,...) -self:F({From,Event,To,ToPointVec2,NearRadius}) -NearRadius=NearRadius or 25 -local Timer=1 -if From=="Loaded"then -if self.CargoObject then -self.CargoObject:Destroy() -end -self.CargoSet:ForEach( -function(Cargo,NearRadius) -Cargo:__UnBoard(Timer,ToPointVec2,NearRadius) -Timer=Timer+10 -end,{NearRadius} -) -self:__UnBoarding(1,ToPointVec2,NearRadius,...) -end -end -function CARGO_GROUP:onleaveUnBoarding(From,Event,To,ToPointVec2,NearRadius,...) -self:F({From,Event,To,ToPointVec2,NearRadius}) -local Angle=180 -local Speed=10 -local Distance=5 -if From=="UnBoarding"then -local UnBoarded=true -for CargoID,Cargo in pairs(self.CargoSet:GetSet())do -self:T(Cargo.current) -if not Cargo:is("UnLoaded")then -UnBoarded=false -end -end -if UnBoarded then -return true -else -self:__UnBoarding(1,ToPointVec2,NearRadius,...) -end -return false -end -end -function CARGO_GROUP:onafterUnBoarding(From,Event,To,ToPointVec2,NearRadius,...) -self:F({From,Event,To,ToPointVec2,NearRadius}) -self:__UnLoad(1,ToPointVec2,...) -end -function CARGO_GROUP:onenterUnLoaded(From,Event,To,ToPointVec2,...) -self:F({From,Event,To,ToPointVec2}) -if From=="Loaded"then -self.CargoSet:ForEach( -function(Cargo) -Cargo:UnLoad(ToPointVec2) -end -) -end -end -function CARGO_GROUP:RespawnOnDestroyed(RespawnDestroyed) -self:F({"In function RespawnOnDestroyed"}) -if RespawnDestroyed then -self.onenterDestroyed=function(self) -self:F("IN FUNCTION") -self:Respawn() -end -else -self.onenterDestroyed=nil -end -end -end -do -CARGO_PACKAGE={ -ClassName="CARGO_PACKAGE" -} -function CARGO_PACKAGE:New(CargoCarrier,Type,Name,Weight,ReportRadius,NearRadius) -local self=BASE:Inherit(self,CARGO_REPRESENTABLE:New(CargoCarrier,Type,Name,Weight,ReportRadius,NearRadius)) -self:F({Type,Name,Weight,ReportRadius,NearRadius}) -self:T(CargoCarrier) -self.CargoCarrier=CargoCarrier -return self -end -function CARGO_PACKAGE:onafterOnBoard(From,Event,To,CargoCarrier,Speed,BoardDistance,LoadDistance,Angle) -self:F() -self.CargoInAir=self.CargoCarrier:InAir() -self:T(self.CargoInAir) -if not self.CargoInAir then -local Points={} -local StartPointVec2=self.CargoCarrier:GetPointVec2() -local CargoCarrierHeading=CargoCarrier:GetHeading() -local CargoDeployHeading=((CargoCarrierHeading+Angle)>=360)and(CargoCarrierHeading+Angle-360)or(CargoCarrierHeading+Angle) -self:T({CargoCarrierHeading,CargoDeployHeading}) -local CargoDeployPointVec2=CargoCarrier:GetPointVec2():Translate(BoardDistance,CargoDeployHeading) -Points[#Points+1]=StartPointVec2:WaypointGround(Speed) -Points[#Points+1]=CargoDeployPointVec2:WaypointGround(Speed) -local TaskRoute=self.CargoCarrier:TaskRoute(Points) -self.CargoCarrier:SetTask(TaskRoute,1) -end -self:Boarded(CargoCarrier,Speed,BoardDistance,LoadDistance,Angle) -end -function CARGO_PACKAGE:IsNear(CargoCarrier) -self:F() -local CargoCarrierPoint=CargoCarrier:GetPointVec2() -local Distance=CargoCarrierPoint:DistanceFromPointVec2(self.CargoCarrier:GetPointVec2()) -self:T(Distance) -if Distance<=self.NearRadius then -return true -else -return false -end -end -function CARGO_PACKAGE:onafterOnBoarded(From,Event,To,CargoCarrier,Speed,BoardDistance,LoadDistance,Angle) -self:F() -if self:IsNear(CargoCarrier)then -self:__Load(1,CargoCarrier,Speed,LoadDistance,Angle) -else -self:__Boarded(1,CargoCarrier,Speed,BoardDistance,LoadDistance,Angle) -end -end -function CARGO_PACKAGE:onafterUnBoard(From,Event,To,CargoCarrier,Speed,UnLoadDistance,UnBoardDistance,Radius,Angle) -self:F() -self.CargoInAir=self.CargoCarrier:InAir() -self:T(self.CargoInAir) -if not self.CargoInAir then -self:_Next(self.FsmP.UnLoad,UnLoadDistance,Angle) -local Points={} -local StartPointVec2=CargoCarrier:GetPointVec2() -local CargoCarrierHeading=self.CargoCarrier:GetHeading() -local CargoDeployHeading=((CargoCarrierHeading+Angle)>=360)and(CargoCarrierHeading+Angle-360)or(CargoCarrierHeading+Angle) -self:T({CargoCarrierHeading,CargoDeployHeading}) -local CargoDeployPointVec2=StartPointVec2:Translate(UnBoardDistance,CargoDeployHeading) -Points[#Points+1]=StartPointVec2:WaypointGround(Speed) -Points[#Points+1]=CargoDeployPointVec2:WaypointGround(Speed) -local TaskRoute=CargoCarrier:TaskRoute(Points) -CargoCarrier:SetTask(TaskRoute,1) -end -self:__UnBoarded(1,CargoCarrier,Speed) -end -function CARGO_PACKAGE:onafterUnBoarded(From,Event,To,CargoCarrier,Speed) -self:F() -if self:IsNear(CargoCarrier)then -self:__UnLoad(1,CargoCarrier,Speed) -else -self:__UnBoarded(1,CargoCarrier,Speed) -end -end -function CARGO_PACKAGE:onafterLoad(From,Event,To,CargoCarrier,Speed,LoadDistance,Angle) -self:F() -self.CargoCarrier=CargoCarrier -local StartPointVec2=self.CargoCarrier:GetPointVec2() -local CargoCarrierHeading=self.CargoCarrier:GetHeading() -local CargoDeployHeading=((CargoCarrierHeading+Angle)>=360)and(CargoCarrierHeading+Angle-360)or(CargoCarrierHeading+Angle) -local CargoDeployPointVec2=StartPointVec2:Translate(LoadDistance,CargoDeployHeading) -local Points={} -Points[#Points+1]=StartPointVec2:WaypointGround(Speed) -Points[#Points+1]=CargoDeployPointVec2:WaypointGround(Speed) -local TaskRoute=self.CargoCarrier:TaskRoute(Points) -self.CargoCarrier:SetTask(TaskRoute,1) -end -function CARGO_PACKAGE:onafterUnLoad(From,Event,To,CargoCarrier,Speed,Distance,Angle) -self:F() -local StartPointVec2=self.CargoCarrier:GetPointVec2() -local CargoCarrierHeading=self.CargoCarrier:GetHeading() -local CargoDeployHeading=((CargoCarrierHeading+Angle)>=360)and(CargoCarrierHeading+Angle-360)or(CargoCarrierHeading+Angle) -local CargoDeployPointVec2=StartPointVec2:Translate(Distance,CargoDeployHeading) -self.CargoCarrier=CargoCarrier -local Points={} -Points[#Points+1]=StartPointVec2:WaypointGround(Speed) -Points[#Points+1]=CargoDeployPointVec2:WaypointGround(Speed) -local TaskRoute=self.CargoCarrier:TaskRoute(Points) -self.CargoCarrier:SetTask(TaskRoute,1) -end -end -do -SPOT={ -ClassName="SPOT", -} -function SPOT:New(Recce) -local self=BASE:Inherit(self,FSM:New()) -self:F({}) -self:SetStartState("Off") -self:AddTransition("Off","LaseOn","On") -self:AddTransition("On","Lasing","On") -self:AddTransition({"On","Destroyed"},"LaseOff","Off") -self:AddTransition("*","Destroyed","Destroyed") -self.Recce=Recce -self.LaseScheduler=SCHEDULER:New(self) -self:SetEventPriority(5) -self.Lasing=false -return self -end -function SPOT:onafterLaseOn(From,Event,To,Target,LaserCode,Duration) -self:E({"LaseOn",Target,LaserCode,Duration}) -local function StopLase(self) -self:LaseOff() -end -self.Target=Target -self.LaserCode=LaserCode -self.Lasing=true -local RecceDcsUnit=self.Recce:GetDCSObject() -self.SpotIR=Spot.createInfraRed(RecceDcsUnit,{x=0,y=2,z=0},Target:GetPointVec3():AddY(1):GetVec3()) -self.SpotLaser=Spot.createLaser(RecceDcsUnit,{x=0,y=2,z=0},Target:GetPointVec3():AddY(1):GetVec3(),LaserCode) -if Duration then -self.ScheduleID=self.LaseScheduler:Schedule(self,StopLase,{self},Duration) -end -self:HandleEvent(EVENTS.Dead) -self:__Lasing(-1) -end -function SPOT:OnEventDead(EventData) -self:E({Dead=EventData.IniDCSUnitName,Target=self.Target}) -if self.Target then -if EventData.IniDCSUnitName==self.Target:GetName()then -self:E({"Target dead ",self.Target:GetName()}) -self:Destroyed() -self:LaseOff() -end -end -end -function SPOT:onafterLasing(From,Event,To) -if self.Target:IsAlive()then -self.SpotIR:setPoint(self.Target:GetPointVec3():AddY(1):AddY(math.random(-100,100)/100):AddX(math.random(-100,100)/100):GetVec3()) -self.SpotLaser:setPoint(self.Target:GetPointVec3():AddY(1):GetVec3()) -self:__Lasing(-0.2) -else -self:E({"Target is not alive",self.Target:IsAlive()}) -end -end -function SPOT:onafterLaseOff(From,Event,To) -self:E({"Stopped lasing for ",self.Target:GetName(),SpotIR=self.SportIR,SpotLaser=self.SpotLaser}) -self.Lasing=false -self.SpotIR:destroy() -self.SpotLaser:destroy() -self.SpotIR=nil -self.SpotLaser=nil -if self.ScheduleID then -self.LaseScheduler:Stop(self.ScheduleID) -end -self.ScheduleID=nil -self.Target=nil -return self -end -function SPOT:IsLasing() -return self.Lasing -end -end -OBJECT={ -ClassName="OBJECT", -ObjectName="", -} -function OBJECT:New(ObjectName,Test) -local self=BASE:Inherit(self,BASE:New()) -self:F2(ObjectName) -self.ObjectName=ObjectName -return self -end -function OBJECT:GetID() -self:F2(self.ObjectName) -local DCSObject=self:GetDCSObject() -if DCSObject then -local ObjectID=DCSObject:getID() -return ObjectID -end -return nil -end -function OBJECT:Destroy() -self:F2(self.ObjectName) -local DCSObject=self:GetDCSObject() -if DCSObject then -DCSObject:destroy() -end -return nil -end -IDENTIFIABLE={ -ClassName="IDENTIFIABLE", -IdentifiableName="", -} -local _CategoryName={ -[Unit.Category.AIRPLANE]="Airplane", -[Unit.Category.HELICOPTER]="Helicoper", -[Unit.Category.GROUND_UNIT]="Ground Identifiable", -[Unit.Category.SHIP]="Ship", -[Unit.Category.STRUCTURE]="Structure", -} -function IDENTIFIABLE:New(IdentifiableName) -local self=BASE:Inherit(self,OBJECT:New(IdentifiableName)) -self:F2(IdentifiableName) -self.IdentifiableName=IdentifiableName -return self -end -function IDENTIFIABLE:IsAlive() -self:F3(self.IdentifiableName) -local DCSIdentifiable=self:GetDCSObject() -if DCSIdentifiable then -local IdentifiableIsAlive=DCSIdentifiable:isExist() -return IdentifiableIsAlive -end -return false -end -function IDENTIFIABLE:GetName() -self:F2(self.IdentifiableName) -local IdentifiableName=self.IdentifiableName -return IdentifiableName -end -function IDENTIFIABLE:GetTypeName() -self:F2(self.IdentifiableName) -local DCSIdentifiable=self:GetDCSObject() -if DCSIdentifiable then -local IdentifiableTypeName=DCSIdentifiable:getTypeName() -self:T3(IdentifiableTypeName) -return IdentifiableTypeName -end -self:E(self.ClassName.." "..self.IdentifiableName.." not found!") -return nil -end -function IDENTIFIABLE:GetCategory() -self:F2(self.ObjectName) -local DCSObject=self:GetDCSObject() -if DCSObject then -local ObjectCategory=DCSObject:getCategory() -self:T3(ObjectCategory) -return ObjectCategory -end -return nil -end -function IDENTIFIABLE:GetCategoryName() -local DCSIdentifiable=self:GetDCSObject() -if DCSIdentifiable then -local IdentifiableCategoryName=_CategoryName[self:GetDesc().category] -return IdentifiableCategoryName -end -self:E(self.ClassName.." "..self.IdentifiableName.." not found!") -return nil -end -function IDENTIFIABLE:GetCoalition() -self:F2(self.IdentifiableName) -local DCSIdentifiable=self:GetDCSObject() -if DCSIdentifiable then -local IdentifiableCoalition=DCSIdentifiable:getCoalition() -self:T3(IdentifiableCoalition) -return IdentifiableCoalition -end -self:E(self.ClassName.." "..self.IdentifiableName.." not found!") -return nil -end -function IDENTIFIABLE:GetCountry() -self:F2(self.IdentifiableName) -local DCSIdentifiable=self:GetDCSObject() -if DCSIdentifiable then -local IdentifiableCountry=DCSIdentifiable:getCountry() -self:T3(IdentifiableCountry) -return IdentifiableCountry -end -self:E(self.ClassName.." "..self.IdentifiableName.." not found!") -return nil -end -function IDENTIFIABLE:GetDesc() -self:F2(self.IdentifiableName) -local DCSIdentifiable=self:GetDCSObject() -if DCSIdentifiable then -local IdentifiableDesc=DCSIdentifiable:getDesc() -self:T2(IdentifiableDesc) -return IdentifiableDesc -end -self:E(self.ClassName.." "..self.IdentifiableName.." not found!") -return nil -end -function IDENTIFIABLE:GetCallsign() -return'' -end -function IDENTIFIABLE:GetThreatLevel() -return 0,"Scenery" -end -POSITIONABLE={ -ClassName="POSITIONABLE", -PositionableName="", -} -POSITIONABLE.__={} -POSITIONABLE.__.Cargo={} -function POSITIONABLE:New(PositionableName) -local self=BASE:Inherit(self,IDENTIFIABLE:New(PositionableName)) -self.PositionableName=PositionableName -return self -end -function POSITIONABLE:GetPositionVec3() -self:F2(self.PositionableName) -local DCSPositionable=self:GetDCSObject() -if DCSPositionable then -local PositionablePosition=DCSPositionable:getPosition().p -self:T3(PositionablePosition) -return PositionablePosition -end -return nil -end -function POSITIONABLE:GetVec2() -self:F2(self.PositionableName) -local DCSPositionable=self:GetDCSObject() -if DCSPositionable then -local PositionableVec3=DCSPositionable:getPosition().p -local PositionableVec2={} -PositionableVec2.x=PositionableVec3.x -PositionableVec2.y=PositionableVec3.z -self:T2(PositionableVec2) -return PositionableVec2 -end -return nil -end -function POSITIONABLE:GetPointVec2() -self:F2(self.PositionableName) -local DCSPositionable=self:GetDCSObject() -if DCSPositionable then -local PositionableVec3=DCSPositionable:getPosition().p -local PositionablePointVec2=POINT_VEC2:NewFromVec3(PositionableVec3) -self:T2(PositionablePointVec2) -return PositionablePointVec2 -end -return nil -end -function POSITIONABLE:GetPointVec3() -self:F2(self.PositionableName) -local DCSPositionable=self:GetDCSObject() -if DCSPositionable then -local PositionableVec3=self:GetPositionVec3() -local PositionablePointVec3=POINT_VEC3:NewFromVec3(PositionableVec3) -self:T2(PositionablePointVec3) -return PositionablePointVec3 -end -return nil -end -function POSITIONABLE:GetCoordinate() -self:F2(self.PositionableName) -local DCSPositionable=self:GetDCSObject() -if DCSPositionable then -local PositionableVec3=self:GetPositionVec3() -local PositionableCoordinate=COORDINATE:NewFromVec3(PositionableVec3) -PositionableCoordinate:SetHeading(self:GetHeading()) -PositionableCoordinate:SetVelocity(self:GetVelocityMPS()) -self:T2(PositionableCoordinate) -return PositionableCoordinate -end -return nil -end -function POSITIONABLE:GetRandomVec3(Radius) -self:F2(self.PositionableName) -local DCSPositionable=self:GetDCSObject() -if DCSPositionable then -local PositionablePointVec3=DCSPositionable:getPosition().p -if Radius then -local PositionableRandomVec3={} -local angle=math.random()*math.pi*2; -PositionableRandomVec3.x=PositionablePointVec3.x+math.cos(angle)*math.random()*Radius; -PositionableRandomVec3.y=PositionablePointVec3.y -PositionableRandomVec3.z=PositionablePointVec3.z+math.sin(angle)*math.random()*Radius; -self:T3(PositionableRandomVec3) -return PositionableRandomVec3 -else -self:E("Radius is nil, returning the PointVec3 of the POSITIONABLE",PositionablePointVec3) -return PositionablePointVec3 -end -end -return nil -end -function POSITIONABLE:GetVec3() -self:F2(self.PositionableName) -local DCSPositionable=self:GetDCSObject() -if DCSPositionable then -local PositionableVec3=DCSPositionable:getPosition().p -self:T3(PositionableVec3) -return PositionableVec3 -end -return nil -end -function POSITIONABLE:GetBoundingBox() -self:F2() -local DCSPositionable=self:GetDCSObject() -if DCSPositionable then -local PositionableDesc=DCSPositionable:getDesc() -if PositionableDesc then -local PositionableBox=PositionableDesc.box -return PositionableBox -end -end -return nil -end -function POSITIONABLE:GetAltitude() -self:F2() -local DCSPositionable=self:GetDCSObject() -if DCSPositionable then -local PositionablePointVec3=DCSPositionable:getPoint() -return PositionablePointVec3.y -end -return nil -end -function POSITIONABLE:IsAboveRunway() -self:F2(self.PositionableName) -local DCSPositionable=self:GetDCSObject() -if DCSPositionable then -local Vec2=self:GetVec2() -local SurfaceType=land.getSurfaceType(Vec2) -local IsAboveRunway=SurfaceType==land.SurfaceType.RUNWAY -self:T2(IsAboveRunway) -return IsAboveRunway -end -return nil -end -function POSITIONABLE:GetHeading() -local DCSPositionable=self:GetDCSObject() -if DCSPositionable then -local PositionablePosition=DCSPositionable:getPosition() -if PositionablePosition then -local PositionableHeading=math.atan2(PositionablePosition.x.z,PositionablePosition.x.x) -if PositionableHeading<0 then -PositionableHeading=PositionableHeading+2*math.pi -end -PositionableHeading=PositionableHeading*180/math.pi -self:T2(PositionableHeading) -return PositionableHeading -end -end -return nil -end -function POSITIONABLE:InAir() -self:F2(self.PositionableName) -return nil -end -function POSITIONABLE:GetVelocity() -self:F2(self.PositionableName) -local DCSPositionable=self:GetDCSObject() -if DCSPositionable then -local PositionableVelocityVec3=DCSPositionable:getVelocity() -self:T3(PositionableVelocityVec3) -return PositionableVelocityVec3 -end -return nil -end -function POSITIONABLE:GetHeight() -self:F2(self.PositionableName) -local DCSPositionable=self:GetDCSObject() -if DCSPositionable then -local PositionablePosition=DCSPositionable:getPosition() -if PositionablePosition then -local PositionableHeight=PositionablePosition.p.y -self:T2(PositionableHeight) -return PositionableHeight -end -end -return nil -end -function POSITIONABLE:GetVelocityKMH() -self:F2(self.PositionableName) -local DCSPositionable=self:GetDCSObject() -if DCSPositionable then -local VelocityVec3=self:GetVelocity() -local Velocity=(VelocityVec3.x^2+VelocityVec3.y^2+VelocityVec3.z^2)^0.5 -local Velocity=Velocity*3.6 -self:T3(Velocity) -return Velocity -end -return 0 -end -function POSITIONABLE:GetVelocityMPS() -self:F2(self.PositionableName) -local DCSPositionable=self:GetDCSObject() -if DCSPositionable then -local VelocityVec3=self:GetVelocity() -local Velocity=(VelocityVec3.x^2+VelocityVec3.y^2+VelocityVec3.z^2)^0.5 -self:T3(Velocity) -return Velocity -end -return 0 -end -function POSITIONABLE:GetMessageText(Message,Name) -local DCSObject=self:GetDCSObject() -if DCSObject then -Name=Name and(" ("..Name..")")or"" -local Callsign=string.format("[%s]",self:GetCallsign()~=""and self:GetCallsign()or self:GetName()) -local MessageText=Callsign..Name..": "..Message -return MessageText -end -return nil -end -function POSITIONABLE:GetMessage(Message,Duration,Name) -local DCSObject=self:GetDCSObject() -if DCSObject then -local MessageText=self:GetMessageText(Message,Name) -return MESSAGE:New(MessageText,Duration) -end -return nil -end -function POSITIONABLE:GetMessageType(Message,MessageType,Name) -local DCSObject=self:GetDCSObject() -if DCSObject then -local MessageText=self:GetMessageText(Message,Name) -return MESSAGE:NewType(MessageText,MessageType) -end -return nil -end -function POSITIONABLE:MessageToAll(Message,Duration,Name) -self:F2({Message,Duration}) -local DCSObject=self:GetDCSObject() -if DCSObject then -self:GetMessage(Message,Duration,Name):ToAll() -end -return nil -end -function POSITIONABLE:MessageToCoalition(Message,Duration,MessageCoalition) -self:F2({Message,Duration}) -local Name="" -local DCSObject=self:GetDCSObject() -if DCSObject then -if MessageCoalition==coalition.side.BLUE then -Name="Blue coalition" -end -if MessageCoalition==coalition.side.RED then -Name="Red coalition" -end -self:GetMessage(Message,Duration,Name):ToCoalition(MessageCoalition) -end -return nil -end -function POSITIONABLE:MessageTypeToCoalition(Message,MessageType,MessageCoalition) -self:F2({Message,MessageType}) -local Name="" -local DCSObject=self:GetDCSObject() -if DCSObject then -if MessageCoalition==coalition.side.BLUE then -Name="Blue coalition" -end -if MessageCoalition==coalition.side.RED then -Name="Red coalition" -end -self:GetMessageType(Message,MessageType,Name):ToCoalition(MessageCoalition) -end -return nil -end -function POSITIONABLE:MessageToRed(Message,Duration,Name) -self:F2({Message,Duration}) -local DCSObject=self:GetDCSObject() -if DCSObject then -self:GetMessage(Message,Duration,Name):ToRed() -end -return nil -end -function POSITIONABLE:MessageToBlue(Message,Duration,Name) -self:F2({Message,Duration}) -local DCSObject=self:GetDCSObject() -if DCSObject then -self:GetMessage(Message,Duration,Name):ToBlue() -end -return nil -end -function POSITIONABLE:MessageToClient(Message,Duration,Client,Name) -self:F2({Message,Duration}) -local DCSObject=self:GetDCSObject() -if DCSObject then -self:GetMessage(Message,Duration,Name):ToClient(Client) -end -return nil -end -function POSITIONABLE:MessageToGroup(Message,Duration,MessageGroup,Name) -self:F2({Message,Duration}) -local DCSObject=self:GetDCSObject() -if DCSObject then -if DCSObject:isExist()then -self:GetMessage(Message,Duration,Name):ToGroup(MessageGroup) -end -end -return nil -end -function POSITIONABLE:MessageTypeToGroup(Message,MessageType,MessageGroup,Name) -self:F2({Message,MessageType}) -local DCSObject=self:GetDCSObject() -if DCSObject then -if DCSObject:isExist()then -self:GetMessageType(Message,MessageType,Name):ToGroup(MessageGroup) -end -end -return nil -end -function POSITIONABLE:MessageToSetGroup(Message,Duration,MessageSetGroup,Name) -self:F2({Message,Duration}) -local DCSObject=self:GetDCSObject() -if DCSObject then -if DCSObject:isExist()then -MessageSetGroup:ForEachGroup( -function(MessageGroup) -self:GetMessage(Message,Duration,Name):ToGroup(MessageGroup) -end -) -end -end -return nil -end -function POSITIONABLE:Message(Message,Duration,Name) -self:F2({Message,Duration}) -local DCSObject=self:GetDCSObject() -if DCSObject then -self:GetMessage(Message,Duration,Name):ToGroup(self) -end -return nil -end -function POSITIONABLE:GetRadio() -self:F2(self) -return RADIO:New(self) -end -function POSITIONABLE:GetBeacon() -self:F2(self) -return BEACON:New(self) -end -function POSITIONABLE:LaseUnit(Target,LaserCode,Duration) -self:F2() -LaserCode=LaserCode or math.random(1000,9999) -local RecceDcsUnit=self:GetDCSObject() -local TargetVec3=Target:GetVec3() -self:E("bulding spot") -self.Spot=SPOT:New(self) -self.Spot:LaseOn(Target,LaserCode,Duration) -self.LaserCode=LaserCode -return self.Spot -end -function POSITIONABLE:LaseOff() -self:F2() -if self.Spot then -self.Spot:LaseOff() -self.Spot=nil -end -return self -end -function POSITIONABLE:IsLasing() -self:F2() -local Lasing=false -if self.Spot then -Lasing=self.Spot:IsLasing() -end -return Lasing -end -function POSITIONABLE:GetSpot() -return self.Spot -end -function POSITIONABLE:GetLaserCode() -return self.LaserCode -end -function POSITIONABLE:AddCargo(Cargo) -self.__.Cargo[Cargo]=Cargo -return self -end -function POSITIONABLE:RemoveCargo(Cargo) -self.__.Cargo[Cargo]=nil -return self -end -function POSITIONABLE:HasCargo(Cargo) -return self.__.Cargo[Cargo] -end -function POSITIONABLE:ClearCargo() -self.__.Cargo={} -end -function POSITIONABLE:CargoItemCount() -local ItemCount=0 -for CargoName,Cargo in pairs(self.__.Cargo)do -ItemCount=ItemCount+Cargo:GetCount() -end -return ItemCount -end -function POSITIONABLE:Flare(FlareColor) -self:F2() -trigger.action.signalFlare(self:GetVec3(),FlareColor,0) -end -function POSITIONABLE:FlareWhite() -self:F2() -trigger.action.signalFlare(self:GetVec3(),trigger.flareColor.White,0) -end -function POSITIONABLE:FlareYellow() -self:F2() -trigger.action.signalFlare(self:GetVec3(),trigger.flareColor.Yellow,0) -end -function POSITIONABLE:FlareGreen() -self:F2() -trigger.action.signalFlare(self:GetVec3(),trigger.flareColor.Green,0) -end -function POSITIONABLE:FlareRed() -self:F2() -local Vec3=self:GetVec3() -if Vec3 then -trigger.action.signalFlare(Vec3,trigger.flareColor.Red,0) -end -end -function POSITIONABLE:Smoke(SmokeColor,Range,AddHeight) -self:F2() -if Range then -local Vec3=self:GetRandomVec3(Range) -Vec3.y=Vec3.y+AddHeight or 0 -trigger.action.smoke(Vec3,SmokeColor) -else -local Vec3=self:GetVec3() -Vec3.y=Vec3.y+AddHeight or 0 -trigger.action.smoke(self:GetVec3(),SmokeColor) -end -end -function POSITIONABLE:SmokeGreen() -self:F2() -trigger.action.smoke(self:GetVec3(),trigger.smokeColor.Green) -end -function POSITIONABLE:SmokeRed() -self:F2() -trigger.action.smoke(self:GetVec3(),trigger.smokeColor.Red) -end -function POSITIONABLE:SmokeWhite() -self:F2() -trigger.action.smoke(self:GetVec3(),trigger.smokeColor.White) -end -function POSITIONABLE:SmokeOrange() -self:F2() -trigger.action.smoke(self:GetVec3(),trigger.smokeColor.Orange) -end -function POSITIONABLE:SmokeBlue() -self:F2() -trigger.action.smoke(self:GetVec3(),trigger.smokeColor.Blue) -end -CONTROLLABLE={ -ClassName="CONTROLLABLE", -ControllableName="", -WayPointFunctions={}, -} -function CONTROLLABLE:New(ControllableName) -local self=BASE:Inherit(self,POSITIONABLE:New(ControllableName)) -self:F2(ControllableName) -self.ControllableName=ControllableName -self.TaskScheduler=SCHEDULER:New(self) -return self -end -function CONTROLLABLE:_GetController() -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local ControllableController=DCSControllable:getController() -return ControllableController -end -return nil -end -function CONTROLLABLE:GetUnits() -self:F2({self.ControllableName}) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local DCSUnits=DCSControllable:getUnits() -local Units={} -for Index,UnitData in pairs(DCSUnits)do -Units[#Units+1]=UNIT:Find(UnitData) -end -self:T3(Units) -return Units -end -return nil -end -function CONTROLLABLE:GetLife() -self:F2(self.ControllableName) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local UnitLife=0 -local Units=self:GetUnits() -if#Units==1 then -local Unit=Units[1] -UnitLife=Unit:GetLife() -else -local UnitLifeTotal=0 -for UnitID,Unit in pairs(Units)do -local Unit=Unit -UnitLifeTotal=UnitLifeTotal+Unit:GetLife() -end -UnitLife=UnitLifeTotal/#Units -end -return UnitLife -end -return nil -end -function CONTROLLABLE:GetLife0() -self:F2(self.ControllableName) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local UnitLife=0 -local Units=self:GetUnits() -if#Units==1 then -local Unit=Units[1] -UnitLife=Unit:GetLife0() -else -local UnitLifeTotal=0 -for UnitID,Unit in pairs(Units)do -local Unit=Unit -UnitLifeTotal=UnitLifeTotal+Unit:GetLife0() -end -UnitLife=UnitLifeTotal/#Units -end -return UnitLife -end -return nil -end -function CONTROLLABLE:GetFuel() -self:F(self.ControllableName) -return nil -end -function CONTROLLABLE:ClearTasks() -self:F2() -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local Controller=self:_GetController() -Controller:resetTask() -return self -end -return nil -end -function CONTROLLABLE:PopCurrentTask() -self:F2() -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local Controller=self:_GetController() -Controller:popTask() -return self -end -return nil -end -function CONTROLLABLE:PushTask(DCSTask,WaitTime) -self:F2() -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local Controller=self:_GetController() -if WaitTime then -self.TaskScheduler:Schedule(Controller,Controller.pushTask,{DCSTask},WaitTime) -else -Controller:pushTask(DCSTask) -end -return self -end -return nil -end -function CONTROLLABLE:SetTask(DCSTask,WaitTime) -self:F2({DCSTask=DCSTask}) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local DCSControllableName=self:GetName() -local function SetTask(Controller,DCSTask) -if self and self:IsAlive()then -local Controller=self:_GetController() -Controller:setTask(DCSTask) -else -BASE:E(DCSControllableName.." is not alive anymore. Cannot set DCSTask "..DCSTask) -end -end -if not WaitTime or WaitTime==0 then -SetTask(self,DCSTask) -else -self.TaskScheduler:Schedule(self,SetTask,{DCSTask},WaitTime) -end -return self -end -return nil -end -function CONTROLLABLE:HasTask() -local HasTaskResult=false -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local Controller=self:_GetController() -HasTaskResult=Controller:hasTask() -end -return HasTaskResult -end -function CONTROLLABLE:TaskCondition(time,userFlag,userFlagValue,condition,duration,lastWayPoint) -self:F2({time,userFlag,userFlagValue,condition,duration,lastWayPoint}) -local DCSStopCondition={} -DCSStopCondition.time=time -DCSStopCondition.userFlag=userFlag -DCSStopCondition.userFlagValue=userFlagValue -DCSStopCondition.condition=condition -DCSStopCondition.duration=duration -DCSStopCondition.lastWayPoint=lastWayPoint -self:T3({DCSStopCondition}) -return DCSStopCondition -end -function CONTROLLABLE:TaskControlled(DCSTask,DCSStopCondition) -self:F2({DCSTask,DCSStopCondition}) -local DCSTaskControlled -DCSTaskControlled={ -id='ControlledTask', -params={ -task=DCSTask, -stopCondition=DCSStopCondition -} -} -self:T3({DCSTaskControlled}) -return DCSTaskControlled -end -function CONTROLLABLE:TaskCombo(DCSTasks) -self:F2({DCSTasks}) -local DCSTaskCombo -DCSTaskCombo={ -id='ComboTask', -params={ -tasks=DCSTasks -} -} -for TaskID,Task in ipairs(DCSTasks)do -self:T(Task) -end -self:T3({DCSTaskCombo}) -return DCSTaskCombo -end -function CONTROLLABLE:TaskWrappedAction(DCSCommand,Index) -self:F2({DCSCommand}) -local DCSTaskWrappedAction -DCSTaskWrappedAction={ -id="WrappedAction", -enabled=true, -number=Index or 1, -auto=false, -params={ -action=DCSCommand, -}, -} -self:T3({DCSTaskWrappedAction}) -return DCSTaskWrappedAction -end -function CONTROLLABLE:SetTaskWaypoint(Waypoint,Task) -Waypoint.task=self:TaskCombo({Task}) -self:T3({Waypoint.task}) -return Waypoint.task -end -function CONTROLLABLE:SetCommand(DCSCommand) -self:F2(DCSCommand) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local Controller=self:_GetController() -Controller:setCommand(DCSCommand) -return self -end -return nil -end -function CONTROLLABLE:CommandSwitchWayPoint(FromWayPoint,ToWayPoint) -self:F2({FromWayPoint,ToWayPoint}) -local CommandSwitchWayPoint={ -id='SwitchWaypoint', -params={ -fromWaypointIndex=FromWayPoint, -goToWaypointIndex=ToWayPoint, -}, -} -self:T3({CommandSwitchWayPoint}) -return CommandSwitchWayPoint -end -function CONTROLLABLE:CommandStopRoute(StopRoute) -self:F2({StopRoute}) -local CommandStopRoute={ -id='StopRoute', -params={ -value=StopRoute, -}, -} -self:T3({CommandStopRoute}) -return CommandStopRoute -end -function CONTROLLABLE:TaskAttackGroup(AttackGroup,WeaponType,WeaponExpend,AttackQty,Direction,Altitude,AttackQtyLimit) -self:F2({self.ControllableName,AttackGroup,WeaponType,WeaponExpend,AttackQty,Direction,Altitude,AttackQtyLimit}) -local DirectionEnabled=nil -if Direction then -DirectionEnabled=true -end -local AltitudeEnabled=nil -if Altitude then -AltitudeEnabled=true -end -local DCSTask -DCSTask={id='AttackGroup', -params={ -groupId=AttackGroup:GetID(), -weaponType=WeaponType, -expend=WeaponExpend, -attackQty=AttackQty, -directionEnabled=DirectionEnabled, -direction=Direction, -altitudeEnabled=AltitudeEnabled, -altitude=Altitude, -attackQtyLimit=AttackQtyLimit, -}, -}, -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:TaskAttackUnit(AttackUnit,GroupAttack,WeaponExpend,AttackQty,Direction,Altitude,Visible,WeaponType) -self:F2({self.ControllableName,AttackUnit,GroupAttack,WeaponExpend,AttackQty,Direction,Altitude,Visible,WeaponType}) -local DCSTask -DCSTask={ -id='AttackUnit', -params={ -unitId=AttackUnit:GetID(), -groupAttack=GroupAttack or false, -visible=Visible or false, -expend=WeaponExpend or"Auto", -directionEnabled=Direction and true or false, -direction=Direction, -altitudeEnabled=Altitude and true or false, -altitude=Altitude or 30, -attackQtyLimit=AttackQty and true or false, -attackQty=AttackQty, -weaponType=WeaponType -} -} -self:T3(DCSTask) -return DCSTask -end -function CONTROLLABLE:TaskBombing(Vec2,GroupAttack,WeaponExpend,AttackQty,Direction,Altitude,WeaponType) -self:F2({self.ControllableName,Vec2,GroupAttack,WeaponExpend,AttackQty,Direction,Altitude,WeaponType}) -local DCSTask -DCSTask={ -id='Bombing', -params={ -point=Vec2, -groupAttack=GroupAttack or false, -expend=WeaponExpend or"Auto", -attackQtyLimit=AttackQty and true or false, -attackQty=AttackQty, -directionEnabled=Direction and true or false, -direction=Direction, -altitudeEnabled=Altitude and true or false, -altitude=Altitude or 30, -weaponType=WeaponType, -}, -}, -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:TaskAttackMapObject(Vec2,GroupAttack,WeaponExpend,AttackQty,Direction,Altitude,WeaponType) -self:F2({self.ControllableName,Vec2,GroupAttack,WeaponExpend,AttackQty,Direction,Altitude,WeaponType}) -local DCSTask -DCSTask={ -id='AttackMapObject', -params={ -point=Vec2, -groupAttack=GroupAttack or false, -expend=WeaponExpend or"Auto", -attackQtyLimit=AttackQty and true or false, -attackQty=AttackQty, -directionEnabled=Direction and true or false, -direction=Direction, -altitudeEnabled=Altitude and true or false, -altitude=Altitude or 30, -weaponType=WeaponType, -}, -}, -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:TaskOrbitCircleAtVec2(Point,Altitude,Speed) -self:F2({self.ControllableName,Point,Altitude,Speed}) -local LandHeight=land.getHeight(Point) -self:T3({LandHeight}) -local DCSTask={id='Orbit', -params={pattern=AI.Task.OrbitPattern.CIRCLE, -point=Point, -speed=Speed, -altitude=Altitude+LandHeight -} -} -return DCSTask -end -function CONTROLLABLE:TaskOrbitCircle(Altitude,Speed) -self:F2({self.ControllableName,Altitude,Speed}) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local ControllablePoint=self:GetVec2() -return self:TaskOrbitCircleAtVec2(ControllablePoint,Altitude,Speed) -end -return nil -end -function CONTROLLABLE:TaskHoldPosition() -self:F2({self.ControllableName}) -return self:TaskOrbitCircle(30,10) -end -function CONTROLLABLE:TaskBombingRunway(Airbase,WeaponType,WeaponExpend,AttackQty,Direction,ControllableAttack) -self:F2({self.ControllableName,Airbase,WeaponType,WeaponExpend,AttackQty,Direction,ControllableAttack}) -local DCSTask -DCSTask={id='BombingRunway', -params={ -point=Airbase:GetID(), -weaponType=WeaponType, -expend=WeaponExpend, -attackQty=AttackQty, -direction=Direction, -controllableAttack=ControllableAttack, -}, -}, -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:TaskRefueling() -self:F2({self.ControllableName}) -local DCSTask -DCSTask={id='Refueling', -params={ -}, -}, -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:TaskLandAtVec2(Point,Duration) -self:F2({self.ControllableName,Point,Duration}) -local DCSTask -if Duration and Duration>0 then -DCSTask={id='Land', -params={ -point=Point, -durationFlag=true, -duration=Duration, -}, -} -else -DCSTask={id='Land', -params={ -point=Point, -durationFlag=false, -}, -} -end -self:T3(DCSTask) -return DCSTask -end -function CONTROLLABLE:TaskLandAtZone(Zone,Duration,RandomPoint) -self:F2({self.ControllableName,Zone,Duration,RandomPoint}) -local Point -if RandomPoint then -Point=Zone:GetRandomVec2() -else -Point=Zone:GetVec2() -end -local DCSTask=self:TaskLandAtVec2(Point,Duration) -self:T3(DCSTask) -return DCSTask -end -function CONTROLLABLE:TaskFollow(FollowControllable,Vec3,LastWaypointIndex) -self:F2({self.ControllableName,FollowControllable,Vec3,LastWaypointIndex}) -local LastWaypointIndexFlag=false -if LastWaypointIndex then -LastWaypointIndexFlag=true -end -local DCSTask -DCSTask={ -id='Follow', -params={ -groupId=FollowControllable:GetID(), -pos=Vec3, -lastWptIndexFlag=LastWaypointIndexFlag, -lastWptIndex=LastWaypointIndex -} -} -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:TaskEscort(FollowControllable,Vec3,LastWaypointIndex,EngagementDistance,TargetTypes) -self:F2({self.ControllableName,FollowControllable,Vec3,LastWaypointIndex,EngagementDistance,TargetTypes}) -local LastWaypointIndexFlag=false -if LastWaypointIndex then -LastWaypointIndexFlag=true -end -local DCSTask -DCSTask={id='Escort', -params={ -groupId=FollowControllable:GetID(), -pos=Vec3, -lastWptIndexFlag=LastWaypointIndexFlag, -lastWptIndex=LastWaypointIndex, -engagementDistMax=EngagementDistance, -targetTypes=TargetTypes, -}, -}, -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:TaskFireAtPoint(Vec2,Radius,AmmoCount) -self:F2({self.ControllableName,Vec2,Radius,AmmoCount}) -local DCSTask -DCSTask={id='FireAtPoint', -params={ -point=Vec2, -radius=Radius, -expendQty=100, -expendQtyEnabled=false, -} -} -if AmmoCount then -DCSTask.params.expendQty=AmmoCount -DCSTask.params.expendQtyEnabled=true -end -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:TaskHold() -self:F2({self.ControllableName}) -local DCSTask -DCSTask={id='Hold', -params={ -} -} -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:TaskFAC_AttackGroup(AttackGroup,WeaponType,Designation,Datalink) -self:F2({self.ControllableName,AttackGroup,WeaponType,Designation,Datalink}) -local DCSTask -DCSTask={id='FAC_AttackGroup', -params={ -groupId=AttackGroup:GetID(), -weaponType=WeaponType, -designation=Designation, -datalink=Datalink, -} -} -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:EnRouteTaskEngageTargets(Distance,TargetTypes,Priority) -self:F2({self.ControllableName,Distance,TargetTypes,Priority}) -local DCSTask -DCSTask={id='EngageTargets', -params={ -maxDist=Distance, -targetTypes=TargetTypes, -priority=Priority -} -} -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:EnRouteTaskEngageTargetsInZone(Vec2,Radius,TargetTypes,Priority) -self:F2({self.ControllableName,Vec2,Radius,TargetTypes,Priority}) -local DCSTask -DCSTask={id='EngageTargetsInZone', -params={ -point=Vec2, -zoneRadius=Radius, -targetTypes=TargetTypes, -priority=Priority -} -} -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:EnRouteTaskEngageGroup(AttackGroup,Priority,WeaponType,WeaponExpend,AttackQty,Direction,Altitude,AttackQtyLimit) -self:F2({self.ControllableName,AttackGroup,Priority,WeaponType,WeaponExpend,AttackQty,Direction,Altitude,AttackQtyLimit}) -local DirectionEnabled=nil -if Direction then -DirectionEnabled=true -end -local AltitudeEnabled=nil -if Altitude then -AltitudeEnabled=true -end -local DCSTask -DCSTask={id='EngageControllable', -params={ -groupId=AttackGroup:GetID(), -weaponType=WeaponType, -expend=WeaponExpend, -attackQty=AttackQty, -directionEnabled=DirectionEnabled, -direction=Direction, -altitudeEnabled=AltitudeEnabled, -altitude=Altitude, -attackQtyLimit=AttackQtyLimit, -priority=Priority, -}, -}, -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:EnRouteTaskEngageUnit(EngageUnit,Priority,GroupAttack,WeaponExpend,AttackQty,Direction,Altitude,Visible,ControllableAttack) -self:F2({self.ControllableName,EngageUnit,Priority,GroupAttack,WeaponExpend,AttackQty,Direction,Altitude,Visible,ControllableAttack}) -local DCSTask -DCSTask={id='EngageUnit', -params={ -unitId=EngageUnit:GetID(), -priority=Priority or 1, -groupAttack=GroupAttack or false, -visible=Visible or false, -expend=WeaponExpend or"Auto", -directionEnabled=Direction and true or false, -direction=Direction, -altitudeEnabled=Altitude and true or false, -altitude=Altitude, -attackQtyLimit=AttackQty and true or false, -attackQty=AttackQty, -controllableAttack=ControllableAttack, -}, -}, -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:EnRouteTaskAWACS() -self:F2({self.ControllableName}) -local DCSTask -DCSTask={id='AWACS', -params={ -} -} -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:EnRouteTaskTanker() -self:F2({self.ControllableName}) -local DCSTask -DCSTask={id='Tanker', -params={ -} -} -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:EnRouteTaskEWR() -self:F2({self.ControllableName}) -local DCSTask -DCSTask={id='EWR', -params={ -} -} -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:EnRouteTaskFAC_EngageGroup(AttackGroup,Priority,WeaponType,Designation,Datalink) -self:F2({self.ControllableName,AttackGroup,WeaponType,Priority,Designation,Datalink}) -local DCSTask -DCSTask={id='FAC_EngageControllable', -params={ -groupId=AttackGroup:GetID(), -weaponType=WeaponType, -designation=Designation, -datalink=Datalink, -priority=Priority, -} -} -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:EnRouteTaskFAC(Radius,Priority) -self:F2({self.ControllableName,Radius,Priority}) -local DCSTask -DCSTask={id='FAC', -params={ -radius=Radius, -priority=Priority -} -} -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:TaskEmbarking(Point,Duration,EmbarkingControllable) -self:F2({self.ControllableName,Point,Duration,EmbarkingControllable.DCSControllable}) -local DCSTask -DCSTask={id='Embarking', -params={x=Point.x, -y=Point.y, -duration=Duration, -controllablesForEmbarking={EmbarkingControllable.ControllableID}, -durationFlag=true, -distributionFlag=false, -distribution={}, -} -} -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:TaskEmbarkToTransport(Point,Radius) -self:F2({self.ControllableName,Point,Radius}) -local DCSTask -DCSTask={id='EmbarkToTransport', -params={x=Point.x, -y=Point.y, -zoneRadius=Radius, -} -} -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:TaskFunction(FunctionString,...) -self:F2({FunctionString,arg}) -local DCSTask -local DCSScript={} -DCSScript[#DCSScript+1]="local MissionControllable = GROUP:Find( ... ) " -if arg and arg.n>0 then -local ArgumentKey='_'..tostring(arg):match("table: (.*)") -self:SetState(self,ArgumentKey,arg) -DCSScript[#DCSScript+1]="local Arguments = MissionControllable:GetState( MissionControllable, '"..ArgumentKey.."' ) " -DCSScript[#DCSScript+1]=FunctionString.."( MissionControllable, unpack( Arguments ) )" -else -DCSScript[#DCSScript+1]=FunctionString.."( MissionControllable )" -end -DCSTask=self:TaskWrappedAction( -self:CommandDoScript( -table.concat(DCSScript) -) -) -self:T(DCSTask) -return DCSTask -end -function CONTROLLABLE:TaskMission(TaskMission) -self:F2(Points) -local DCSTask -DCSTask={id='Mission',params={TaskMission,},} -self:T3({DCSTask}) -return DCSTask -end -do -function CONTROLLABLE:PatrolRoute() -local PatrolGroup=self -if not self:IsInstanceOf("GROUP")then -PatrolGroup=self:GetGroup() -end -self:E({PatrolGroup=PatrolGroup:GetName()}) -if PatrolGroup:IsGround()or PatrolGroup:IsShip()then -local Waypoints=PatrolGroup:GetTemplateRoutePoints() -local FromCoord=PatrolGroup:GetCoordinate() -local From=FromCoord:WaypointGround(120) -table.insert(Waypoints,1,From) -local TaskRoute=PatrolGroup:TaskFunction("CONTROLLABLE.PatrolRoute") -self:E({Waypoints=Waypoints}) -local Waypoint=Waypoints[#Waypoints] -PatrolGroup:SetTaskWaypoint(Waypoint,TaskRoute) -PatrolGroup:Route(Waypoints) -end -end -function CONTROLLABLE:PatrolRouteRandom(Speed,Formation,ToWaypoint) -local PatrolGroup=self -if not self:IsInstanceOf("GROUP")then -PatrolGroup=self:GetGroup() -end -self:E({PatrolGroup=PatrolGroup:GetName()}) -if PatrolGroup:IsGround()or PatrolGroup:IsShip()then -local Waypoints=PatrolGroup:GetTemplateRoutePoints() -local FromCoord=PatrolGroup:GetCoordinate() -local FromWaypoint=1 -if ToWaypoint then -FromWaypoint=ToWaypoint -end -local ToWaypoint -repeat -ToWaypoint=math.random(1,#Waypoints) -until(ToWaypoint~=FromWaypoint) -self:E({FromWaypoint=FromWaypoint,ToWaypoint=ToWaypoint}) -local Waypoint=Waypoints[ToWaypoint] -local ToCoord=COORDINATE:NewFromVec2({x=Waypoint.x,y=Waypoint.y}) -local Route={} -Route[#Route+1]=FromCoord:WaypointGround(0) -Route[#Route+1]=ToCoord:WaypointGround(Speed,Formation) -local TaskRouteToZone=PatrolGroup:TaskFunction("CONTROLLABLE.PatrolRouteRandom",Speed,Formation,ToWaypoint) -PatrolGroup:SetTaskWaypoint(Route[#Route],TaskRouteToZone) -PatrolGroup:Route(Route,1) -end -end -function CONTROLLABLE:PatrolZones(ZoneList,Speed,Formation) -if not type(ZoneList)=="table"then -ZoneList={ZoneList} -end -local PatrolGroup=self -if not self:IsInstanceOf("GROUP")then -PatrolGroup=self:GetGroup() -end -self:E({PatrolGroup=PatrolGroup:GetName()}) -if PatrolGroup:IsGround()or PatrolGroup:IsShip()then -local Waypoints=PatrolGroup:GetTemplateRoutePoints() -local Waypoint=Waypoints[math.random(1,#Waypoints)] -local FromCoord=PatrolGroup:GetCoordinate() -local RandomZone=ZoneList[math.random(1,#ZoneList)] -local ToCoord=RandomZone:GetRandomCoordinate(10) -local Route={} -Route[#Route+1]=FromCoord:WaypointGround(120) -Route[#Route+1]=ToCoord:WaypointGround(Speed,Formation) -local TaskRouteToZone=PatrolGroup:TaskFunction("CONTROLLABLE.PatrolZones",ZoneList,Speed,Formation) -PatrolGroup:SetTaskWaypoint(Route[#Route],TaskRouteToZone) -PatrolGroup:Route(Route,1) -end -end -end -function CONTROLLABLE:TaskRoute(Points) -self:F2(Points) -local DCSTask -DCSTask={id='Mission',params={route={points=Points,},},} -self:T3({DCSTask}) -return DCSTask -end -function CONTROLLABLE:RouteToVec2(Point,Speed) -self:F2({Point,Speed}) -local ControllablePoint=self:GetUnit(1):GetVec2() -local PointFrom={} -PointFrom.x=ControllablePoint.x -PointFrom.y=ControllablePoint.y -PointFrom.type="Turning Point" -PointFrom.action="Turning Point" -PointFrom.speed=Speed -PointFrom.speed_locked=true -PointFrom.properties={ -["vnav"]=1, -["scale"]=0, -["angle"]=0, -["vangle"]=0, -["steer"]=2, -} -local PointTo={} -PointTo.x=Point.x -PointTo.y=Point.y -PointTo.type="Turning Point" -PointTo.action="Fly Over Point" -PointTo.speed=Speed -PointTo.speed_locked=true -PointTo.properties={ -["vnav"]=1, -["scale"]=0, -["angle"]=0, -["vangle"]=0, -["steer"]=2, -} -local Points={PointFrom,PointTo} -self:T3(Points) -self:Route(Points) -return self -end -function CONTROLLABLE:RouteToVec3(Point,Speed) -self:F2({Point,Speed}) -local ControllableVec3=self:GetUnit(1):GetVec3() -local PointFrom={} -PointFrom.x=ControllableVec3.x -PointFrom.y=ControllableVec3.z -PointFrom.alt=ControllableVec3.y -PointFrom.alt_type="BARO" -PointFrom.type="Turning Point" -PointFrom.action="Turning Point" -PointFrom.speed=Speed -PointFrom.speed_locked=true -PointFrom.properties={ -["vnav"]=1, -["scale"]=0, -["angle"]=0, -["vangle"]=0, -["steer"]=2, -} -local PointTo={} -PointTo.x=Point.x -PointTo.y=Point.z -PointTo.alt=Point.y -PointTo.alt_type="BARO" -PointTo.type="Turning Point" -PointTo.action="Fly Over Point" -PointTo.speed=Speed -PointTo.speed_locked=true -PointTo.properties={ -["vnav"]=1, -["scale"]=0, -["angle"]=0, -["vangle"]=0, -["steer"]=2, -} -local Points={PointFrom,PointTo} -self:T3(Points) -self:Route(Points) -return self -end -function CONTROLLABLE:Route(Route,DelaySeconds) -self:F2(Route) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local RouteTask=self:TaskRoute(Route) -self:SetTask(RouteTask,DelaySeconds or 1) -return self -end -return nil -end -function CONTROLLABLE:RouteGroundTo(ToCoordinate,Speed,Formation,DelaySeconds) -local FromCoordinate=self:GetCoordinate() -local FromWP=FromCoordinate:WaypointGround() -local ToWP=ToCoordinate:WaypointGround(Speed,Formation) -self:Route({FromWP,ToWP},DelaySeconds) -return self -end -function CONTROLLABLE:RouteAirTo(ToCoordinate,AltType,Type,Action,Speed,DelaySeconds) -local FromCoordinate=self:GetCoordinate() -local FromWP=FromCoordinate:WaypointAir() -local ToWP=ToCoordinate:WaypointAir(AltType,Type,Action,Speed) -self:Route({FromWP,ToWP},DelaySeconds) -return self -end -function CONTROLLABLE:TaskRouteToZone(Zone,Randomize,Speed,Formation) -self:F2(Zone) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local ControllablePoint=self:GetVec2() -local PointFrom={} -PointFrom.x=ControllablePoint.x -PointFrom.y=ControllablePoint.y -PointFrom.type="Turning Point" -PointFrom.action=Formation or"Cone" -PointFrom.speed=20/1.6 -local PointTo={} -local ZonePoint -if Randomize then -ZonePoint=Zone:GetRandomVec2() -else -ZonePoint=Zone:GetVec2() -end -PointTo.x=ZonePoint.x -PointTo.y=ZonePoint.y -PointTo.type="Turning Point" -if Formation then -PointTo.action=Formation -else -PointTo.action="Cone" -end -if Speed then -PointTo.speed=Speed -else -PointTo.speed=20/1.6 -end -local Points={PointFrom,PointTo} -self:T3(Points) -self:Route(Points) -return self -end -return nil -end -function CONTROLLABLE:TaskRouteToVec2(Vec2,Speed,Formation) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local ControllablePoint=self:GetVec2() -local PointFrom={} -PointFrom.x=ControllablePoint.x -PointFrom.y=ControllablePoint.y -PointFrom.type="Turning Point" -PointFrom.action=Formation or"Cone" -PointFrom.speed=20/1.6 -local PointTo={} -PointTo.x=Vec2.x -PointTo.y=Vec2.y -PointTo.type="Turning Point" -if Formation then -PointTo.action=Formation -else -PointTo.action="Cone" -end -if Speed then -PointTo.speed=Speed -else -PointTo.speed=60/3.6 -end -local Points={PointFrom,PointTo} -self:T3(Points) -self:Route(Points) -return self -end -return nil -end -function CONTROLLABLE:CommandDoScript(DoScript) -local DCSDoScript={ -id="Script", -params={ -command=DoScript, -}, -} -self:T3(DCSDoScript) -return DCSDoScript -end -function CONTROLLABLE:GetTaskMission() -self:F2(self.ControllableName) -return routines.utils.deepCopy(_DATABASE.Templates.Controllables[self.ControllableName].Template) -end -function CONTROLLABLE:GetTaskRoute() -self:F2(self.ControllableName) -return routines.utils.deepCopy(_DATABASE.Templates.Controllables[self.ControllableName].Template.route.points) -end -function CONTROLLABLE:CopyRoute(Begin,End,Randomize,Radius) -self:F2({Begin,End}) -local Points={} -local ControllableName=string.match(self:GetName(),".*#") -if ControllableName then -ControllableName=ControllableName:sub(1,-2) -else -ControllableName=self:GetName() -end -self:T3({ControllableName}) -local Template=_DATABASE.Templates.Controllables[ControllableName].Template -if Template then -if not Begin then -Begin=0 -end -if not End then -End=0 -end -for TPointID=Begin+1,#Template.route.points-End do -if Template.route.points[TPointID]then -Points[#Points+1]=routines.utils.deepCopy(Template.route.points[TPointID]) -if Randomize then -if not Radius then -Radius=500 -end -Points[#Points].x=Points[#Points].x+math.random(Radius*-1,Radius) -Points[#Points].y=Points[#Points].y+math.random(Radius*-1,Radius) -end -end -end -return Points -else -error("Template not found for Controllable : "..ControllableName) -end -return nil -end -function CONTROLLABLE:GetDetectedTargets(DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK) -self:F2(self.ControllableName) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local DetectionVisual=(DetectVisual and DetectVisual==true)and Controller.Detection.VISUAL or nil -local DetectionOptical=(DetectOptical and DetectOptical==true)and Controller.Detection.OPTICAL or nil -local DetectionRadar=(DetectRadar and DetectRadar==true)and Controller.Detection.RADAR or nil -local DetectionIRST=(DetectIRST and DetectIRST==true)and Controller.Detection.IRST or nil -local DetectionRWR=(DetectRWR and DetectRWR==true)and Controller.Detection.RWR or nil -local DetectionDLINK=(DetectDLINK and DetectDLINK==true)and Controller.Detection.DLINK or nil -self:T({DetectionVisual,DetectionOptical,DetectionRadar,DetectionIRST,DetectionRWR,DetectionDLINK}) -return self:_GetController():getDetectedTargets(DetectionVisual,DetectionOptical,DetectionRadar,DetectionIRST,DetectionRWR,DetectionDLINK) -end -return nil -end -function CONTROLLABLE:IsTargetDetected(DCSObject,DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK) -self:F2(self.ControllableName) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local DetectionVisual=(DetectVisual and DetectVisual==true)and Controller.Detection.VISUAL or nil -local DetectionOptical=(DetectOptical and DetectOptical==true)and Controller.Detection.OPTICAL or nil -local DetectionRadar=(DetectRadar and DetectRadar==true)and Controller.Detection.RADAR or nil -local DetectionIRST=(DetectIRST and DetectIRST==true)and Controller.Detection.IRST or nil -local DetectionRWR=(DetectRWR and DetectRWR==true)and Controller.Detection.RWR or nil -local DetectionDLINK=(DetectDLINK and DetectDLINK==true)and Controller.Detection.DLINK or nil -local Controller=self:_GetController() -local TargetIsDetected,TargetIsVisible,TargetLastTime,TargetKnowType,TargetKnowDistance,TargetLastPos,TargetLastVelocity -=Controller:isTargetDetected(DCSObject,DetectionVisual,DetectionOptical,DetectionRadar,DetectionIRST,DetectionRWR,DetectionDLINK) -return TargetIsDetected,TargetIsVisible,TargetLastTime,TargetKnowType,TargetKnowDistance,TargetLastPos,TargetLastVelocity -end -return nil -end -function CONTROLLABLE:OptionROEHoldFirePossible() -self:F2({self.ControllableName}) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -if self:IsAir()or self:IsGround()or self:IsShip()then -return true -end -return false -end -return nil -end -function CONTROLLABLE:OptionROEHoldFire() -self:F2({self.ControllableName}) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local Controller=self:_GetController() -if self:IsAir()then -Controller:setOption(AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.WEAPON_HOLD) -elseif self:IsGround()then -Controller:setOption(AI.Option.Ground.id.ROE,AI.Option.Ground.val.ROE.WEAPON_HOLD) -elseif self:IsShip()then -Controller:setOption(AI.Option.Naval.id.ROE,AI.Option.Naval.val.ROE.WEAPON_HOLD) -end -return self -end -return nil -end -function CONTROLLABLE:OptionROEReturnFirePossible() -self:F2({self.ControllableName}) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -if self:IsAir()or self:IsGround()or self:IsShip()then -return true -end -return false -end -return nil -end -function CONTROLLABLE:OptionROEReturnFire() -self:F2({self.ControllableName}) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local Controller=self:_GetController() -if self:IsAir()then -Controller:setOption(AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.RETURN_FIRE) -elseif self:IsGround()then -Controller:setOption(AI.Option.Ground.id.ROE,AI.Option.Ground.val.ROE.RETURN_FIRE) -elseif self:IsShip()then -Controller:setOption(AI.Option.Naval.id.ROE,AI.Option.Naval.val.ROE.RETURN_FIRE) -end -return self -end -return nil -end -function CONTROLLABLE:OptionROEOpenFirePossible() -self:F2({self.ControllableName}) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -if self:IsAir()or self:IsGround()or self:IsShip()then -return true -end -return false -end -return nil -end -function CONTROLLABLE:OptionROEOpenFire() -self:F2({self.ControllableName}) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local Controller=self:_GetController() -if self:IsAir()then -Controller:setOption(AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.OPEN_FIRE) -elseif self:IsGround()then -Controller:setOption(AI.Option.Ground.id.ROE,AI.Option.Ground.val.ROE.OPEN_FIRE) -elseif self:IsShip()then -Controller:setOption(AI.Option.Naval.id.ROE,AI.Option.Naval.val.ROE.OPEN_FIRE) -end -return self -end -return nil -end -function CONTROLLABLE:OptionROEWeaponFreePossible() -self:F2({self.ControllableName}) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -if self:IsAir()then -return true -end -return false -end -return nil -end -function CONTROLLABLE:OptionROEWeaponFree() -self:F2({self.ControllableName}) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local Controller=self:_GetController() -if self:IsAir()then -Controller:setOption(AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.WEAPON_FREE) -end -return self -end -return nil -end -function CONTROLLABLE:OptionROTNoReactionPossible() -self:F2({self.ControllableName}) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -if self:IsAir()then -return true -end -return false -end -return nil -end -function CONTROLLABLE:OptionROTNoReaction() -self:F2({self.ControllableName}) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local Controller=self:_GetController() -if self:IsAir()then -Controller:setOption(AI.Option.Air.id.REACTION_ON_THREAT,AI.Option.Air.val.REACTION_ON_THREAT.NO_REACTION) -end -return self -end -return nil -end -function CONTROLLABLE:OptionROTPassiveDefensePossible() -self:F2({self.ControllableName}) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -if self:IsAir()then -return true -end -return false -end -return nil -end -function CONTROLLABLE:OptionROTPassiveDefense() -self:F2({self.ControllableName}) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local Controller=self:_GetController() -if self:IsAir()then -Controller:setOption(AI.Option.Air.id.REACTION_ON_THREAT,AI.Option.Air.val.REACTION_ON_THREAT.PASSIVE_DEFENCE) -end -return self -end -return nil -end -function CONTROLLABLE:OptionROTEvadeFirePossible() -self:F2({self.ControllableName}) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -if self:IsAir()then -return true -end -return false -end -return nil -end -function CONTROLLABLE:OptionROTEvadeFire() -self:F2({self.ControllableName}) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local Controller=self:_GetController() -if self:IsAir()then -Controller:setOption(AI.Option.Air.id.REACTION_ON_THREAT,AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE) -end -return self -end -return nil -end -function CONTROLLABLE:OptionROTVerticalPossible() -self:F2({self.ControllableName}) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -if self:IsAir()then -return true -end -return false -end -return nil -end -function CONTROLLABLE:OptionROTVertical() -self:F2({self.ControllableName}) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local Controller=self:_GetController() -if self:IsAir()then -Controller:setOption(AI.Option.Air.id.REACTION_ON_THREAT,AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE) -end -return self -end -return nil -end -function CONTROLLABLE:OptionRTBBingoFuel(RTB) -self:F2({self.ControllableName}) -RTB=RTB or true -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local Controller=self:_GetController() -if self:IsAir()then -Controller:setOption(AI.Option.Air.id.RTB_ON_BINGO,RTB) -end -return self -end -return nil -end -function CONTROLLABLE:OptionRTBAmmo(WeaponsFlag) -self:F2({self.ControllableName}) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local Controller=self:_GetController() -if self:IsAir()then -Controller:setOption(AI.Option.GROUND.id.RTB_ON_OUT_OF_AMMO,WeaponsFlag) -end -return self -end -return nil -end -function CONTROLLABLE:WayPointInitialize(WayPoints) -self:F({WayPoints}) -if WayPoints then -self.WayPoints=WayPoints -else -self.WayPoints=self:GetTaskRoute() -end -return self -end -function CONTROLLABLE:GetWayPoints() -self:F() -if self.WayPoints then -return self.WayPoints -end -return nil -end -function CONTROLLABLE:WayPointFunction(WayPoint,WayPointIndex,WayPointFunction,...) -self:F2({WayPoint,WayPointIndex,WayPointFunction}) -table.insert(self.WayPoints[WayPoint].task.params.tasks,WayPointIndex) -self.WayPoints[WayPoint].task.params.tasks[WayPointIndex]=self:TaskFunction(WayPointFunction,arg) -return self -end -function CONTROLLABLE:WayPointExecute(WayPoint,WaitTime) -self:F({WayPoint,WaitTime}) -if not WayPoint then -WayPoint=1 -end -for TaskPointID=1,WayPoint-1 do -table.remove(self.WayPoints,1) -end -self:T3(self.WayPoints) -self:SetTask(self:TaskRoute(self.WayPoints),WaitTime) -return self -end -function CONTROLLABLE:IsAirPlane() -self:F2() -local DCSObject=self:GetDCSObject() -if DCSObject then -local Category=DCSObject:getDesc().category -return Category==Unit.Category.AIRPLANE -end -return nil -end -function CONTROLLABLE:GetSize() -local DCSObject=self:GetDCSObject() -if DCSObject then -return 1 -else -return 0 -end -end -GROUP={ -ClassName="GROUP", -} -GROUP.Takeoff={ -Air=1, -Runway=2, -Hot=3, -Cold=4, -} -GROUPTEMPLATE={} -GROUPTEMPLATE.Takeoff={ -[GROUP.Takeoff.Air]={"Turning Point","Turning Point"}, -[GROUP.Takeoff.Runway]={"TakeOff","From Runway"}, -[GROUP.Takeoff.Hot]={"TakeOffParkingHot","From Parking Area Hot"}, -[GROUP.Takeoff.Cold]={"TakeOffParking","From Parking Area"} -} -function GROUP:Register(GroupName) -self=BASE:Inherit(self,CONTROLLABLE:New(GroupName)) -self:F2(GroupName) -self.GroupName=GroupName -self:SetEventPriority(4) -return self -end -function GROUP:Find(DCSGroup) -local GroupName=DCSGroup:getName() -local GroupFound=_DATABASE:FindGroup(GroupName) -return GroupFound -end -function GROUP:FindByName(GroupName) -local GroupFound=_DATABASE:FindGroup(GroupName) -return GroupFound -end -function GROUP:GetDCSObject() -local DCSGroup=Group.getByName(self.GroupName) -if DCSGroup then -return DCSGroup -end -return nil -end -function GROUP:GetPositionVec3() -self:F2(self.PositionableName) -local DCSPositionable=self:GetDCSObject() -if DCSPositionable then -local PositionablePosition=DCSPositionable:getUnits()[1]:getPosition().p -self:T3(PositionablePosition) -return PositionablePosition -end -return nil -end -function GROUP:IsAlive() -self:F2(self.GroupName) -local DCSGroup=self:GetDCSObject() -if DCSGroup then -if DCSGroup:isExist()then -local DCSUnit=DCSGroup:getUnit(1) -if DCSUnit then -local GroupIsAlive=DCSUnit:isActive() -self:T3(GroupIsAlive) -return GroupIsAlive -end -end -end -return nil -end -function GROUP:Destroy() -self:F2(self.GroupName) -local DCSGroup=self:GetDCSObject() -if DCSGroup then -for Index,UnitData in pairs(DCSGroup:getUnits())do -self:CreateEventCrash(timer.getTime(),UnitData) -end -DCSGroup:destroy() -DCSGroup=nil -end -return nil -end -function GROUP:GetCategory() -self:F2(self.GroupName) -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local GroupCategory=DCSGroup:getCategory() -self:T3(GroupCategory) -return GroupCategory -end -return nil -end -function GROUP:GetCategoryName() -self:F2(self.GroupName) -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local CategoryNames={ -[Group.Category.AIRPLANE]="Airplane", -[Group.Category.HELICOPTER]="Helicopter", -[Group.Category.GROUND]="Ground Unit", -[Group.Category.SHIP]="Ship", -} -local GroupCategory=DCSGroup:getCategory() -self:T3(GroupCategory) -return CategoryNames[GroupCategory] -end -return nil -end -function GROUP:GetCoalition() -self:F2(self.GroupName) -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local GroupCoalition=DCSGroup:getCoalition() -self:T3(GroupCoalition) -return GroupCoalition -end -return nil -end -function GROUP:GetCountry() -self:F2(self.GroupName) -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local GroupCountry=DCSGroup:getUnit(1):getCountry() -self:T3(GroupCountry) -return GroupCountry -end -return nil -end -function GROUP:GetUnit(UnitNumber) -self:F2({self.GroupName,UnitNumber}) -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local DCSUnit=DCSGroup:getUnit(UnitNumber) -local UnitFound=UNIT:Find(DCSGroup:getUnit(UnitNumber)) -self:T2(UnitFound) -return UnitFound -end -return nil -end -function GROUP:GetDCSUnit(UnitNumber) -self:F2({self.GroupName,UnitNumber}) -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local DCSUnitFound=DCSGroup:getUnit(UnitNumber) -self:T3(DCSUnitFound) -return DCSUnitFound -end -return nil -end -function GROUP:GetSize() -self:F2({self.GroupName}) -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local GroupSize=DCSGroup:getSize() -if GroupSize then -self:T3(GroupSize) -return GroupSize -else -return 0 -end -end -return nil -end -function GROUP:GetInitialSize() -self:F2({self.GroupName}) -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local GroupInitialSize=DCSGroup:getInitialSize() -self:T3(GroupInitialSize) -return GroupInitialSize -end -return nil -end -function GROUP:GetDCSUnits() -self:F2({self.GroupName}) -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local DCSUnits=DCSGroup:getUnits() -self:T3(DCSUnits) -return DCSUnits -end -return nil -end -function GROUP:Activate() -self:F2({self.GroupName}) -trigger.action.activateGroup(self:GetDCSObject()) -return self:GetDCSObject() -end -function GROUP:GetTypeName() -self:F2(self.GroupName) -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local GroupTypeName=DCSGroup:getUnit(1):getTypeName() -self:T3(GroupTypeName) -return(GroupTypeName) -end -return nil -end -function GROUP:GetPlayerName() -self:F2(self.GroupName) -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local PlayerName=DCSGroup:getUnit(1):getPlayerName() -self:T3(PlayerName) -return(PlayerName) -end -return nil -end -function GROUP:GetCallsign() -self:F2(self.GroupName) -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local GroupCallSign=DCSGroup:getUnit(1):getCallsign() -self:T3(GroupCallSign) -return GroupCallSign -end -return nil -end -function GROUP:GetVec2() -self:F2(self.GroupName) -local UnitPoint=self:GetUnit(1) -UnitPoint:GetVec2() -local GroupPointVec2=UnitPoint:GetVec2() -self:T3(GroupPointVec2) -return GroupPointVec2 -end -function GROUP:GetVec3() -self:F2(self.GroupName) -local GroupVec3=self:GetUnit(1):GetVec3() -self:T3(GroupVec3) -return GroupVec3 -end -function GROUP:GetPointVec2() -self:F2(self.GroupName) -local FirstUnit=self:GetUnit(1) -if FirstUnit then -local FirstUnitPointVec2=FirstUnit:GetPointVec2() -self:T3(FirstUnitPointVec2) -return FirstUnitPointVec2 -end -return nil -end -function GROUP:GetCoordinate() -self:F2(self.PositionableName) -local FirstUnit=self:GetUnit(1) -if FirstUnit then -local FirstUnitCoordinate=FirstUnit:GetCoordinate() -self:T3(FirstUnitCoordinate) -return FirstUnitCoordinate -end -return nil -end -function GROUP:GetRandomVec3(Radius) -self:F2(self.GroupName) -local FirstUnit=self:GetUnit(1) -if FirstUnit then -local FirstUnitRandomPointVec3=FirstUnit:GetRandomVec3(Radius) -self:T3(FirstUnitRandomPointVec3) -return FirstUnitRandomPointVec3 -end -return nil -end -function GROUP:GetHeading() -self:F2(self.GroupName) -local GroupSize=self:GetSize() -local HeadingAccumulator=0 -if GroupSize then -for i=1,GroupSize do -HeadingAccumulator=HeadingAccumulator+self:GetUnit(i):GetHeading() -end -return math.floor(HeadingAccumulator/GroupSize) -end -return nil -end -function GROUP:GetFuel() -self:F(self.ControllableName) -local DCSControllable=self:GetDCSObject() -if DCSControllable then -local GroupSize=self:GetSize() -local TotalFuel=0 -for UnitID,UnitData in pairs(self:GetUnits())do -local Unit=UnitData -local UnitFuel=Unit:GetFuel() -self:F({Fuel=UnitFuel}) -TotalFuel=TotalFuel+UnitFuel -end -local GroupFuel=TotalFuel/GroupSize -return GroupFuel -end -return 0 -end -do -function GROUP:IsCompletelyInZone(Zone) -self:F2({self.GroupName,Zone}) -if not self:IsAlive()then return false end -for UnitID,UnitData in pairs(self:GetUnits())do -local Unit=UnitData -if Zone:IsVec3InZone(Unit:GetVec3())then -else -return false -end -end -return true -end -function GROUP:IsPartlyInZone(Zone) -self:F2({self.GroupName,Zone}) -local IsOneUnitInZone=false -local IsOneUnitOutsideZone=false -if not self:IsAlive()then return false end -for UnitID,UnitData in pairs(self:GetUnits())do -local Unit=UnitData -if Zone:IsVec3InZone(Unit:GetVec3())then -IsOneUnitInZone=true -else -IsOneUnitOutsideZone=true -end -end -if IsOneUnitInZone and IsOneUnitOutsideZone then -return true -else -return false -end -end -function GROUP:IsNotInZone(Zone) -self:F2({self.GroupName,Zone}) -if not self:IsAlive()then return true end -for UnitID,UnitData in pairs(self:GetUnits())do -local Unit=UnitData -if Zone:IsVec3InZone(Unit:GetVec3())then -return false -end -end -return true -end -function GROUP:CountInZone(Zone) -self:F2({self.GroupName,Zone}) -local Count=0 -if not self:IsAlive()then return Count end -for UnitID,UnitData in pairs(self:GetUnits())do -local Unit=UnitData -if Zone:IsVec3InZone(Unit:GetVec3())then -Count=Count+1 -end -end -return Count -end -function GROUP:IsAir() -self:F2(self.GroupName) -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local IsAirResult=DCSGroup:getCategory()==Group.Category.AIRPLANE or DCSGroup:getCategory()==Group.Category.HELICOPTER -self:T3(IsAirResult) -return IsAirResult -end -return nil -end -function GROUP:IsHelicopter() -self:F2(self.GroupName) -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local GroupCategory=DCSGroup:getCategory() -self:T2(GroupCategory) -return GroupCategory==Group.Category.HELICOPTER -end -return nil -end -function GROUP:IsAirPlane() -self:F2() -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local GroupCategory=DCSGroup:getCategory() -self:T2(GroupCategory) -return GroupCategory==Group.Category.AIRPLANE -end -return nil -end -function GROUP:IsGround() -self:F2() -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local GroupCategory=DCSGroup:getCategory() -self:T2(GroupCategory) -return GroupCategory==Group.Category.GROUND -end -return nil -end -function GROUP:IsShip() -self:F2() -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local GroupCategory=DCSGroup:getCategory() -self:T2(GroupCategory) -return GroupCategory==Group.Category.SHIP -end -return nil -end -function GROUP:AllOnGround() -self:F2() -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local AllOnGroundResult=true -for Index,UnitData in pairs(DCSGroup:getUnits())do -if UnitData:inAir()then -AllOnGroundResult=false -end -end -self:T3(AllOnGroundResult) -return AllOnGroundResult -end -return nil -end -end -do -function GROUP:SetAIOnOff(AIOnOff) -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local DCSController=DCSGroup:getController() -if DCSController then -DCSController:setOnOff(AIOnOff) -return self -end -end -return nil -end -function GROUP:SetAIOn() -return self:SetAIOnOff(true) -end -function GROUP:SetAIOff() -return self:SetAIOnOff(false) -end -end -function GROUP:GetMaxVelocity() -self:F2() -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local GroupVelocityMax=0 -for Index,UnitData in pairs(DCSGroup:getUnits())do -local UnitVelocityVec3=UnitData:getVelocity() -local UnitVelocity=math.abs(UnitVelocityVec3.x)+math.abs(UnitVelocityVec3.y)+math.abs(UnitVelocityVec3.z) -if UnitVelocity>GroupVelocityMax then -GroupVelocityMax=UnitVelocity -end -end -return GroupVelocityMax -end -return nil -end -function GROUP:GetMinHeight() -self:F2() -end -function GROUP:GetMaxHeight() -self:F2() -end -function GROUP:Respawn(Template) -if self:IsAlive()then -local Vec3=self:GetVec3() -Template.x=Vec3.x -Template.y=Vec3.z -self:E(#Template.units) -for UnitID,UnitData in pairs(self:GetUnits())do -local GroupUnit=UnitData -self:E(GroupUnit:GetName()) -if GroupUnit:IsAlive()then -local GroupUnitVec3=GroupUnit:GetVec3() -local GroupUnitHeading=GroupUnit:GetHeading() -Template.units[UnitID].alt=GroupUnitVec3.y -Template.units[UnitID].x=GroupUnitVec3.x -Template.units[UnitID].y=GroupUnitVec3.z -Template.units[UnitID].heading=GroupUnitHeading -self:E({UnitID,Template.units[UnitID],Template.units[UnitID]}) -end -end -end -self:Destroy() -_DATABASE:Spawn(Template) -self:ResetEvents() -end -function GROUP:GetTemplate() -local GroupName=self:GetName() -return UTILS.DeepCopy(_DATABASE:GetGroupTemplate(GroupName)) -end -function GROUP:GetTemplateRoutePoints() -local GroupName=self:GetName() -return UTILS.DeepCopy(_DATABASE:GetGroupTemplate(GroupName).route.points) -end -function GROUP:SetTemplateControlled(Template,Controlled) -Template.uncontrolled=not Controlled -return Template -end -function GROUP:SetTemplateCountry(Template,CountryID) -Template.CountryID=CountryID -return Template -end -function GROUP:SetTemplateCoalition(Template,CoalitionID) -Template.CoalitionID=CoalitionID -return Template -end -function GROUP:GetTaskMission() -self:F2(self.GroupName) -return routines.utils.deepCopy(_DATABASE.Templates.Groups[self.GroupName].Template) -end -function GROUP:GetTaskRoute() -self:F2(self.GroupName) -return routines.utils.deepCopy(_DATABASE.Templates.Groups[self.GroupName].Template.route.points) -end -function GROUP:CopyRoute(Begin,End,Randomize,Radius) -self:F2({Begin,End}) -local Points={} -local GroupName=string.match(self:GetName(),".*#") -if GroupName then -GroupName=GroupName:sub(1,-2) -else -GroupName=self:GetName() -end -self:T3({GroupName}) -local Template=_DATABASE.Templates.Groups[GroupName].Template -if Template then -if not Begin then -Begin=0 -end -if not End then -End=0 -end -for TPointID=Begin+1,#Template.route.points-End do -if Template.route.points[TPointID]then -Points[#Points+1]=routines.utils.deepCopy(Template.route.points[TPointID]) -if Randomize then -if not Radius then -Radius=500 -end -Points[#Points].x=Points[#Points].x+math.random(Radius*-1,Radius) -Points[#Points].y=Points[#Points].y+math.random(Radius*-1,Radius) -end -end -end -return Points -else -error("Template not found for Group : "..GroupName) -end -return nil -end -function GROUP:CalculateThreatLevelA2G() -local MaxThreatLevelA2G=0 -for UnitName,UnitData in pairs(self:GetUnits())do -local ThreatUnit=UnitData -local ThreatLevelA2G=ThreatUnit:GetThreatLevel() -if ThreatLevelA2G>MaxThreatLevelA2G then -MaxThreatLevelA2G=ThreatLevelA2G -end -end -self:T3(MaxThreatLevelA2G) -return MaxThreatLevelA2G -end -function GROUP:InAir() -self:F2(self.GroupName) -local DCSGroup=self:GetDCSObject() -if DCSGroup then -local DCSUnit=DCSGroup:getUnit(1) -if DCSUnit then -local GroupInAir=DCSGroup:getUnit(1):inAir() -self:T3(GroupInAir) -return GroupInAir -end -end -return nil -end -do -function GROUP:RouteRTB(RTBAirbase,Speed) -self:F2({RTBAirbase,Speed}) -local DCSGroup=self:GetDCSObject() -if DCSGroup then -if RTBAirbase then -local GroupPoint=self:GetVec2() -local GroupVelocity=self:GetUnit(1):GetDesc().speedMax -local PointFrom={} -PointFrom.x=GroupPoint.x -PointFrom.y=GroupPoint.y -PointFrom.type="Turning Point" -PointFrom.action="Turning Point" -PointFrom.speed=GroupVelocity -local PointTo={} -local AirbasePointVec2=RTBAirbase:GetPointVec2() -local AirbaseAirPoint=AirbasePointVec2:WaypointAir( -POINT_VEC3.RoutePointAltType.BARO, -"Land", -"Landing", -Speed or self:GetUnit(1):GetDesc().speedMax -) -AirbaseAirPoint["airdromeId"]=RTBAirbase:GetID() -AirbaseAirPoint["speed_locked"]=true, -self:E(AirbaseAirPoint) -local Points={PointFrom,AirbaseAirPoint} -self:T3(Points) -local Template=self:GetTemplate() -Template.route.points=Points -self:Respawn(Template) -self:Route(Points) -self:Respawn(Template) -else -self:ClearTasks() -end -end -return self -end -end -function GROUP:OnReSpawn(ReSpawnFunction) -self.ReSpawnFunction=ReSpawnFunction -end -do -function GROUP:HandleEvent(Event,EventFunction,...) -self:EventDispatcher():OnEventForGroup(self:GetName(),EventFunction,self,Event,...) -return self -end -function GROUP:UnHandleEvent(Event) -self:EventDispatcher():RemoveEvent(self,Event) -return self -end -function GROUP:ResetEvents() -self:EventDispatcher():Reset(self) -for UnitID,UnitData in pairs(self:GetUnits())do -UnitData:ResetEvents() -end -return self -end -end -do -function GROUP:GetPlayerNames() -local PlayerNames={} -local Units=self:GetUnits() -for UnitID,UnitData in pairs(Units)do -local Unit=UnitData -local PlayerName=Unit:GetPlayerName() -if PlayerName and PlayerName~=""then -PlayerNames=PlayerNames or{} -table.insert(PlayerNames,PlayerName) -end -end -self:F2(PlayerNames) -return PlayerNames -end -end -UNIT={ -ClassName="UNIT", -} -function UNIT:Register(UnitName) -local self=BASE:Inherit(self,CONTROLLABLE:New(UnitName)) -self.UnitName=UnitName -self:SetEventPriority(3) -return self -end -function UNIT:Find(DCSUnit) -local UnitName=DCSUnit:getName() -local UnitFound=_DATABASE:FindUnit(UnitName) -return UnitFound -end -function UNIT:FindByName(UnitName) -local UnitFound=_DATABASE:FindUnit(UnitName) -return UnitFound -end -function UNIT:Name() -return self.UnitName -end -function UNIT:GetDCSObject() -local DCSUnit=Unit.getByName(self.UnitName) -if DCSUnit then -return DCSUnit -end -return nil -end -function UNIT:ReSpawn(SpawnVec3,Heading) -local SpawnGroupTemplate=UTILS.DeepCopy(_DATABASE:GetGroupTemplateFromUnitName(self:Name())) -self:T(SpawnGroupTemplate) -local SpawnGroup=self:GetGroup() -if SpawnGroup then -local Vec3=SpawnGroup:GetVec3() -SpawnGroupTemplate.x=SpawnVec3.x -SpawnGroupTemplate.y=SpawnVec3.z -self:E(#SpawnGroupTemplate.units) -for UnitID,UnitData in pairs(SpawnGroup:GetUnits())do -local GroupUnit=UnitData -self:E(GroupUnit:GetName()) -if GroupUnit:IsAlive()then -local GroupUnitVec3=GroupUnit:GetVec3() -local GroupUnitHeading=GroupUnit:GetHeading() -SpawnGroupTemplate.units[UnitID].alt=GroupUnitVec3.y -SpawnGroupTemplate.units[UnitID].x=GroupUnitVec3.x -SpawnGroupTemplate.units[UnitID].y=GroupUnitVec3.z -SpawnGroupTemplate.units[UnitID].heading=GroupUnitHeading -self:E({UnitID,SpawnGroupTemplate.units[UnitID],SpawnGroupTemplate.units[UnitID]}) -end -end -end -for UnitTemplateID,UnitTemplateData in pairs(SpawnGroupTemplate.units)do -self:T(UnitTemplateData.name) -if UnitTemplateData.name==self:Name()then -self:T("Adjusting") -SpawnGroupTemplate.units[UnitTemplateID].alt=SpawnVec3.y -SpawnGroupTemplate.units[UnitTemplateID].x=SpawnVec3.x -SpawnGroupTemplate.units[UnitTemplateID].y=SpawnVec3.z -SpawnGroupTemplate.units[UnitTemplateID].heading=Heading -self:E({UnitTemplateID,SpawnGroupTemplate.units[UnitTemplateID],SpawnGroupTemplate.units[UnitTemplateID]}) -else -self:E(SpawnGroupTemplate.units[UnitTemplateID].name) -local GroupUnit=UNIT:FindByName(SpawnGroupTemplate.units[UnitTemplateID].name) -if GroupUnit and GroupUnit:IsAlive()then -local GroupUnitVec3=GroupUnit:GetVec3() -local GroupUnitHeading=GroupUnit:GetHeading() -UnitTemplateData.alt=GroupUnitVec3.y -UnitTemplateData.x=GroupUnitVec3.x -UnitTemplateData.y=GroupUnitVec3.z -UnitTemplateData.heading=GroupUnitHeading -else -if SpawnGroupTemplate.units[UnitTemplateID].name~=self:Name()then -self:T("nilling") -SpawnGroupTemplate.units[UnitTemplateID].delete=true -end -end -end -end -local i=1 -while i<=#SpawnGroupTemplate.units do -local UnitTemplateData=SpawnGroupTemplate.units[i] -self:T(UnitTemplateData.name) -if UnitTemplateData.delete then -table.remove(SpawnGroupTemplate.units,i) -else -i=i+1 -end -end -_DATABASE:Spawn(SpawnGroupTemplate) -end -function UNIT:IsActive() -self:F2(self.UnitName) -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local UnitIsActive=DCSUnit:isActive() -return UnitIsActive -end -return nil -end -function UNIT:IsAlive() -self:F3(self.UnitName) -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local UnitIsAlive=DCSUnit:isExist()and DCSUnit:isActive() -return UnitIsAlive -end -return nil -end -function UNIT:GetCallsign() -self:F2(self.UnitName) -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local UnitCallSign=DCSUnit:getCallsign() -return UnitCallSign -end -self:E(self.ClassName.." "..self.UnitName.." not found!") -return nil -end -function UNIT:GetPlayerName() -self:F2(self.UnitName) -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local PlayerName=DCSUnit:getPlayerName() -if PlayerName==nil then -PlayerName="" -end -return PlayerName -end -return nil -end -function UNIT:GetNumber() -self:F2(self.UnitName) -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local UnitNumber=DCSUnit:getNumber() -return UnitNumber -end -return nil -end -function UNIT:GetGroup() -self:F2(self.UnitName) -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local UnitGroup=GROUP:Find(DCSUnit:getGroup()) -return UnitGroup -end -return nil -end -function UNIT:GetPrefix() -self:F2(self.UnitName) -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local UnitPrefix=string.match(self.UnitName,".*#"):sub(1,-2) -self:T3(UnitPrefix) -return UnitPrefix -end -return nil -end -function UNIT:GetAmmo() -self:F2(self.UnitName) -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local UnitAmmo=DCSUnit:getAmmo() -return UnitAmmo -end -return nil -end -function UNIT:GetSensors() -self:F2(self.UnitName) -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local UnitSensors=DCSUnit:getSensors() -return UnitSensors -end -return nil -end -function UNIT:HasSensors(...) -self:F2(arg) -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local HasSensors=DCSUnit:hasSensors(unpack(arg)) -return HasSensors -end -return nil -end -function UNIT:HasSEAD() -self:F2() -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local UnitSEADAttributes=DCSUnit:getDesc().attributes -local HasSEAD=false -if UnitSEADAttributes["RADAR_BAND1_FOR_ARM"]and UnitSEADAttributes["RADAR_BAND1_FOR_ARM"]==true or -UnitSEADAttributes["RADAR_BAND2_FOR_ARM"]and UnitSEADAttributes["RADAR_BAND2_FOR_ARM"]==true then -HasSEAD=true -end -return HasSEAD -end -return nil -end -function UNIT:GetRadar() -self:F2(self.UnitName) -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local UnitRadarOn,UnitRadarObject=DCSUnit:getRadar() -return UnitRadarOn,UnitRadarObject -end -return nil,nil -end -function UNIT:GetFuel() -self:F(self.UnitName) -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local UnitFuel=DCSUnit:getFuel() -return UnitFuel -end -return nil -end -function UNIT:GetUnits() -self:F2({self.UnitName}) -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local DCSUnits=DCSUnit:getUnits() -local Units={} -Units[1]=UNIT:Find(DCSUnit) -self:T3(Units) -return Units -end -return nil -end -function UNIT:GetLife() -self:F2(self.UnitName) -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local UnitLife=DCSUnit:getLife() -return UnitLife -end -return-1 -end -function UNIT:GetLife0() -self:F2(self.UnitName) -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local UnitLife0=DCSUnit:getLife0() -return UnitLife0 -end -return 0 -end -function UNIT:GetCategoryName() -self:F3(self.UnitName) -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local CategoryNames={ -[Unit.Category.AIRPLANE]="Airplane", -[Unit.Category.HELICOPTER]="Helicopter", -[Unit.Category.GROUND_UNIT]="Ground Unit", -[Unit.Category.SHIP]="Ship", -[Unit.Category.STRUCTURE]="Structure", -} -local UnitCategory=DCSUnit:getDesc().category -self:T3(UnitCategory) -return CategoryNames[UnitCategory] -end -return nil -end -function UNIT:GetThreatLevel() -local ThreatLevel=0 -local ThreatText="" -local Descriptor=self:GetDesc() -if Descriptor then -local Attributes=Descriptor.attributes -self:T(Attributes) -if self:IsGround()then -self:T("Ground") -local ThreatLevels={ -"Unarmed", -"Infantry", -"Old Tanks & APCs", -"Tanks & IFVs without ATGM", -"Tanks & IFV with ATGM", -"Modern Tanks", -"AAA", -"IR Guided SAMs", -"SR SAMs", -"MR SAMs", -"LR SAMs" -} -if Attributes["LR SAM"]then ThreatLevel=10 -elseif Attributes["MR SAM"]then ThreatLevel=9 -elseif Attributes["SR SAM"]and -not Attributes["IR Guided SAM"]then ThreatLevel=8 -elseif(Attributes["SR SAM"]or Attributes["MANPADS"])and -Attributes["IR Guided SAM"]then ThreatLevel=7 -elseif Attributes["AAA"]then ThreatLevel=6 -elseif Attributes["Modern Tanks"]then ThreatLevel=5 -elseif(Attributes["Tanks"]or Attributes["IFV"])and -Attributes["ATGM"]then ThreatLevel=4 -elseif(Attributes["Tanks"]or Attributes["IFV"])and -not Attributes["ATGM"]then ThreatLevel=3 -elseif Attributes["Old Tanks"]or Attributes["APC"]or Attributes["Artillery"]then ThreatLevel=2 -elseif Attributes["Infantry"]then ThreatLevel=1 -end -ThreatText=ThreatLevels[ThreatLevel+1] -end -if self:IsAir()then -self:T("Air") -local ThreatLevels={ -"Unarmed", -"Tanker", -"AWACS", -"Transport Helicopter", -"UAV", -"Bomber", -"Strategic Bomber", -"Attack Helicopter", -"Battleplane", -"Multirole Fighter", -"Fighter" -} -if Attributes["Fighters"]then ThreatLevel=10 -elseif Attributes["Multirole fighters"]then ThreatLevel=9 -elseif Attributes["Battleplanes"]then ThreatLevel=8 -elseif Attributes["Attack helicopters"]then ThreatLevel=7 -elseif Attributes["Strategic bombers"]then ThreatLevel=6 -elseif Attributes["Bombers"]then ThreatLevel=5 -elseif Attributes["UAVs"]then ThreatLevel=4 -elseif Attributes["Transport helicopters"]then ThreatLevel=3 -elseif Attributes["AWACS"]then ThreatLevel=2 -elseif Attributes["Tankers"]then ThreatLevel=1 -end -ThreatText=ThreatLevels[ThreatLevel+1] -end -if self:IsShip()then -self:T("Ship") -local ThreatLevels={ -"Unarmed ship", -"Light armed ships", -"Corvettes", -"", -"Frigates", -"", -"Cruiser", -"", -"Destroyer", -"", -"Aircraft Carrier" -} -if Attributes["Aircraft Carriers"]then ThreatLevel=10 -elseif Attributes["Destroyers"]then ThreatLevel=8 -elseif Attributes["Cruisers"]then ThreatLevel=6 -elseif Attributes["Frigates"]then ThreatLevel=4 -elseif Attributes["Corvettes"]then ThreatLevel=2 -elseif Attributes["Light armed ships"]then ThreatLevel=1 -end -ThreatText=ThreatLevels[ThreatLevel+1] -end -end -self:T2(ThreatLevel) -return ThreatLevel,ThreatText -end -function UNIT:IsInZone(Zone) -self:F2({self.UnitName,Zone}) -if self:IsAlive()then -local IsInZone=Zone:IsVec3InZone(self:GetVec3()) -self:T2({IsInZone}) -return IsInZone -end -return false -end -function UNIT:IsNotInZone(Zone) -self:F2({self.UnitName,Zone}) -if self:IsAlive()then -local IsInZone=not Zone:IsVec3InZone(self:GetVec3()) -self:T({IsInZone}) -return IsInZone -else -return false -end -end -function UNIT:OtherUnitInRadius(AwaitUnit,Radius) -self:F2({self.UnitName,AwaitUnit.UnitName,Radius}) -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local UnitVec3=self:GetVec3() -local AwaitUnitVec3=AwaitUnit:GetVec3() -if(((UnitVec3.x-AwaitUnitVec3.x)^2+(UnitVec3.z-AwaitUnitVec3.z)^2)^0.5<=Radius)then -self:T3("true") -return true -else -self:T3("false") -return false -end -end -return nil -end -function UNIT:IsAir() -self:F2() -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local UnitDescriptor=DCSUnit:getDesc() -self:T3({UnitDescriptor.category,Unit.Category.AIRPLANE,Unit.Category.HELICOPTER}) -local IsAirResult=(UnitDescriptor.category==Unit.Category.AIRPLANE)or(UnitDescriptor.category==Unit.Category.HELICOPTER) -self:T3(IsAirResult) -return IsAirResult -end -return nil -end -function UNIT:IsGround() -self:F2() -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local UnitDescriptor=DCSUnit:getDesc() -self:T3({UnitDescriptor.category,Unit.Category.GROUND_UNIT}) -local IsGroundResult=(UnitDescriptor.category==Unit.Category.GROUND_UNIT) -self:T3(IsGroundResult) -return IsGroundResult -end -return nil -end -function UNIT:IsFriendly(FriendlyCoalition) -self:F2() -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local UnitCoalition=DCSUnit:getCoalition() -self:T3({UnitCoalition,FriendlyCoalition}) -local IsFriendlyResult=(UnitCoalition==FriendlyCoalition) -self:E(IsFriendlyResult) -return IsFriendlyResult -end -return nil -end -function UNIT:IsShip() -self:F2() -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local UnitDescriptor=DCSUnit:getDesc() -self:T3({UnitDescriptor.category,Unit.Category.SHIP}) -local IsShipResult=(UnitDescriptor.category==Unit.Category.SHIP) -self:T3(IsShipResult) -return IsShipResult -end -return nil -end -function UNIT:InAir() -self:F2(self.UnitName) -local DCSUnit=self:GetDCSObject() -if DCSUnit then -local UnitInAir=DCSUnit:inAir() -self:T3(UnitInAir) -return UnitInAir -end -return nil -end -do -function UNIT:HandleEvent(Event,EventFunction) -self:EventDispatcher():OnEventForUnit(self:GetName(),EventFunction,self,Event) -return self -end -function UNIT:UnHandleEvent(Event) -self:EventDispatcher():RemoveForUnit(self:GetName(),self,Event) -return self -end -function UNIT:ResetEvents() -self:EventDispatcher():Reset(self) -return self -end -end -do -function UNIT:IsDetected(TargetUnit) -local TargetIsDetected,TargetIsVisible,TargetLastTime,TargetKnowType,TargetKnowDistance,TargetLastPos,TargetLastVelocity=self:IsTargetDetected(TargetUnit:GetDCSObject()) -return TargetIsDetected -end -function UNIT:IsLOS(TargetUnit) -local IsLOS=self:GetPointVec3():IsLOS(TargetUnit:GetPointVec3()) -return IsLOS -end -end -CLIENT={ -ONBOARDSIDE={ -NONE=0, -LEFT=1, -RIGHT=2, -BACK=3, -FRONT=4 -}, -ClassName="CLIENT", -ClientName=nil, -ClientAlive=false, -ClientTransport=false, -ClientBriefingShown=false, -_Menus={}, -_Tasks={}, -Messages={ -} -} -function CLIENT:Find(DCSUnit,Error) -local ClientName=DCSUnit:getName() -local ClientFound=_DATABASE:FindClient(ClientName) -if ClientFound then -ClientFound:F(ClientName) -return ClientFound -end -if not Error then -error("CLIENT not found for: "..ClientName) -end -end -function CLIENT:FindByName(ClientName,ClientBriefing,Error) -local ClientFound=_DATABASE:FindClient(ClientName) -if ClientFound then -ClientFound:F({ClientName,ClientBriefing}) -ClientFound:AddBriefing(ClientBriefing) -ClientFound.MessageSwitch=true -return ClientFound -end -if not Error then -error("CLIENT not found for: "..ClientName) -end -end -function CLIENT:Register(ClientName) -local self=BASE:Inherit(self,UNIT:Register(ClientName)) -self:F(ClientName) -self.ClientName=ClientName -self.MessageSwitch=true -self.ClientAlive2=false -self.AliveCheckScheduler=SCHEDULER:New(self,self._AliveCheckScheduler,{"Client Alive "..ClientName},1,5) -self:E(self) -return self -end -function CLIENT:Transport() -self:F() -self.ClientTransport=true -return self -end -function CLIENT:AddBriefing(ClientBriefing) -self:F(ClientBriefing) -self.ClientBriefing=ClientBriefing -self.ClientBriefingShown=false -return self -end -function CLIENT:ShowBriefing() -self:F({self.ClientName,self.ClientBriefingShown}) -if not self.ClientBriefingShown then -self.ClientBriefingShown=true -local Briefing="" -if self.ClientBriefing then -Briefing=Briefing..self.ClientBriefing -end -Briefing=Briefing.." Press [LEFT ALT]+[B] to view the complete mission briefing." -self:Message(Briefing,60,"Briefing") -end -return self -end -function CLIENT:ShowMissionBriefing(MissionBriefing) -self:F({self.ClientName}) -if MissionBriefing then -self:Message(MissionBriefing,60,"Mission Briefing") -end -return self -end -function CLIENT:Reset(ClientName) -self:F() -self._Menus={} -end -function CLIENT:IsMultiSeated() -self:F(self.ClientName) -local ClientMultiSeatedTypes={ -["Mi-8MT"]="Mi-8MT", -["UH-1H"]="UH-1H", -["P-51B"]="P-51B" -} -if self:IsAlive()then -local ClientTypeName=self:GetClientGroupUnit():GetTypeName() -if ClientMultiSeatedTypes[ClientTypeName]then -return true -end -end -return false -end -function CLIENT:Alive(CallBackFunction,...) -self:F() -self.ClientCallBack=CallBackFunction -self.ClientParameters=arg -return self -end -function CLIENT:_AliveCheckScheduler(SchedulerName) -self:F3({SchedulerName,self.ClientName,self.ClientAlive2,self.ClientBriefingShown,self.ClientCallBack}) -if self:IsAlive()then -if self.ClientAlive2==false then -self:ShowBriefing() -if self.ClientCallBack then -self:T("Calling Callback function") -self.ClientCallBack(self,unpack(self.ClientParameters)) -end -self.ClientAlive2=true -end -else -if self.ClientAlive2==true then -self.ClientAlive2=false -end -end -return true -end -function CLIENT:GetDCSGroup() -self:F3() -local ClientUnit=Unit.getByName(self.ClientName) -local CoalitionsData={AlivePlayersRed=coalition.getPlayers(coalition.side.RED),AlivePlayersBlue=coalition.getPlayers(coalition.side.BLUE)} -for CoalitionId,CoalitionData in pairs(CoalitionsData)do -self:T3({"CoalitionData:",CoalitionData}) -for UnitId,UnitData in pairs(CoalitionData)do -self:T3({"UnitData:",UnitData}) -if UnitData and UnitData:isExist()then -if ClientUnit then -local ClientGroup=ClientUnit:getGroup() -if ClientGroup then -self:T3("ClientGroup = "..self.ClientName) -if ClientGroup:isExist()and UnitData:getGroup():isExist()then -if ClientGroup:getID()==UnitData:getGroup():getID()then -self:T3("Normal logic") -self:T3(self.ClientName.." : group found!") -self.ClientGroupID=ClientGroup:getID() -self.ClientGroupName=ClientGroup:getName() -return ClientGroup -end -else -self:T3("Bug 1.5 logic") -local ClientGroupTemplate=_DATABASE.Templates.Units[self.ClientName].GroupTemplate -self.ClientGroupID=ClientGroupTemplate.groupId -self.ClientGroupName=_DATABASE.Templates.Units[self.ClientName].GroupName -self:T3(self.ClientName.." : group found in bug 1.5 resolvement logic!") -return ClientGroup -end -end -else -end -end -end -end -if ClientUnit then -local ClientGroup=ClientUnit:getGroup() -if ClientGroup then -self:T3("ClientGroup = "..self.ClientName) -if ClientGroup:isExist()then -self:T3("Normal logic") -self:T3(self.ClientName.." : group found!") -return ClientGroup -end -end -end -self.ClientGroupID=nil -self.ClientGroupUnit=nil -return nil -end -function CLIENT:GetClientGroupID() -local ClientGroup=self:GetDCSGroup() -return self.ClientGroupID -end -function CLIENT:GetClientGroupName() -local ClientGroup=self:GetDCSGroup() -self:T(self.ClientGroupName) -return self.ClientGroupName -end -function CLIENT:GetClientGroupUnit() -self:F2() -local ClientDCSUnit=Unit.getByName(self.ClientName) -self:T(self.ClientDCSUnit) -if ClientDCSUnit and ClientDCSUnit:isExist()then -local ClientUnit=_DATABASE:FindUnit(self.ClientName) -self:T2(ClientUnit) -return ClientUnit -end -end -function CLIENT:GetClientGroupDCSUnit() -self:F2() -local ClientDCSUnit=Unit.getByName(self.ClientName) -if ClientDCSUnit and ClientDCSUnit:isExist()then -self:T2(ClientDCSUnit) -return ClientDCSUnit -end -end -function CLIENT:IsTransport() -self:F() -return self.ClientTransport -end -function CLIENT:ShowCargo() -self:F() -local CargoMsg="" -for CargoName,Cargo in pairs(CARGOS)do -if self==Cargo:IsLoadedInClient()then -CargoMsg=CargoMsg..Cargo.CargoName.." Type:"..Cargo.CargoType.." Weight: "..Cargo.CargoWeight.."\n" -end -end -if CargoMsg==""then -CargoMsg="empty" -end -self:Message(CargoMsg,15,"Co-Pilot: Cargo Status",30) -end -function CLIENT.SwitchMessages(PrmTable) -PrmTable[1].MessageSwitch=PrmTable[2] -end -function CLIENT:Message(Message,MessageDuration,MessageCategory,MessageInterval,MessageID) -self:F({Message,MessageDuration,MessageCategory,MessageInterval}) -if self.MessageSwitch==true then -if MessageCategory==nil then -MessageCategory="Messages" -end -if MessageID~=nil then -if self.Messages[MessageID]==nil then -self.Messages[MessageID]={} -self.Messages[MessageID].MessageId=MessageID -self.Messages[MessageID].MessageTime=timer.getTime() -self.Messages[MessageID].MessageDuration=MessageDuration -if MessageInterval==nil then -self.Messages[MessageID].MessageInterval=600 -else -self.Messages[MessageID].MessageInterval=MessageInterval -end -MESSAGE:New(Message,MessageDuration,MessageCategory):ToClient(self) -else -if self:GetClientGroupDCSUnit()and not self:GetClientGroupDCSUnit():inAir()then -if timer.getTime()-self.Messages[MessageID].MessageTime>=self.Messages[MessageID].MessageDuration+10 then -MESSAGE:New(Message,MessageDuration,MessageCategory):ToClient(self) -self.Messages[MessageID].MessageTime=timer.getTime() -end -else -if timer.getTime()-self.Messages[MessageID].MessageTime>=self.Messages[MessageID].MessageDuration+self.Messages[MessageID].MessageInterval then -MESSAGE:New(Message,MessageDuration,MessageCategory):ToClient(self) -self.Messages[MessageID].MessageTime=timer.getTime() -end -end -end -else -MESSAGE:New(Message,MessageDuration,MessageCategory):ToClient(self) -end -end -end -STATIC={ -ClassName="STATIC", -} -function STATIC:FindByName(StaticName,RaiseError) -local StaticFound=_DATABASE:FindStatic(StaticName) -self.StaticName=StaticName -if StaticFound then -StaticFound:F3({StaticName}) -return StaticFound -end -if RaiseError==nil or RaiseError==true then -error("STATIC not found for: "..StaticName) -end -return nil -end -function STATIC:Register(StaticName) -local self=BASE:Inherit(self,POSITIONABLE:New(StaticName)) -self.StaticName=StaticName -return self -end -function STATIC:GetDCSObject() -local DCSStatic=StaticObject.getByName(self.StaticName) -if DCSStatic then -return DCSStatic -end -return nil -end -function STATIC:GetThreatLevel() -return 1,"Static" -end -AIRBASE={ -ClassName="AIRBASE", -CategoryName={ -[Airbase.Category.AIRDROME]="Airdrome", -[Airbase.Category.HELIPAD]="Helipad", -[Airbase.Category.SHIP]="Ship", -}, -} -AIRBASE.Caucasus={ -["Gelendzhik"]="Gelendzhik", -["Krasnodar_Pashkovsky"]="Krasnodar-Pashkovsky", -["Sukhumi_Babushara"]="Sukhumi-Babushara", -["Gudauta"]="Gudauta", -["Batumi"]="Batumi", -["Senaki_Kolkhi"]="Senaki-Kolkhi", -["Kobuleti"]="Kobuleti", -["Kutaisi"]="Kutaisi", -["Tbilisi_Lochini"]="Tbilisi-Lochini", -["Soganlug"]="Soganlug", -["Vaziani"]="Vaziani", -["Anapa_Vityazevo"]="Anapa-Vityazevo", -["Krasnodar_Center"]="Krasnodar-Center", -["Novorossiysk"]="Novorossiysk", -["Krymsk"]="Krymsk", -["Maykop_Khanskaya"]="Maykop-Khanskaya", -["Sochi_Adler"]="Sochi-Adler", -["Mineralnye_Vody"]="Mineralnye Vody", -["Nalchik"]="Nalchik", -["Mozdok"]="Mozdok", -["Beslan"]="Beslan", -} -AIRBASE.Nevada={ -["Creech_AFB"]="Creech AFB", -["Groom_Lake_AFB"]="Groom Lake AFB", -["McCarran_International_Airport"]="McCarran International Airport", -["Nellis_AFB"]="Nellis AFB", -["Beatty_Airport"]="Beatty Airport", -["Boulder_City_Airport"]="Boulder City Airport", -["Echo_Bay"]="Echo Bay", -["Henderson_Executive_Airport"]="Henderson Executive Airport", -["Jean_Airport"]="Jean Airport", -["Laughlin_Airport"]="Laughlin Airport", -["Lincoln_County"]="Lincoln County", -["Mellan_Airstrip"]="Mellan Airstrip", -["Mesquite"]="Mesquite", -["Mina_Airport_3Q0"]="Mina Airport 3Q0", -["North_Las_Vegas"]="North Las Vegas", -["Pahute_Mesa_Airstrip"]="Pahute Mesa Airstrip", -["Tonopah_Airport"]="Tonopah Airport", -["Tonopah_Test_Range_Airfield"]="Tonopah Test Range Airfield", -} -AIRBASE.Normandy={ -["Saint_Pierre_du_Mont"]="Saint Pierre du Mont", -["Lignerolles"]="Lignerolles", -["Cretteville"]="Cretteville", -["Maupertus"]="Maupertus", -["Brucheville"]="Brucheville", -["Meautis"]="Meautis", -["Cricqueville_en_Bessin"]="Cricqueville-en-Bessin", -["Lessay"]="Lessay", -["Sainte_Laurent_sur_Mer"]="Sainte-Laurent-sur-Mer", -["Biniville"]="Biniville", -["Cardonville"]="Cardonville", -["Deux_Jumeaux"]="Deux Jumeaux", -["Chippelle"]="Chippelle", -["Beuzeville"]="Beuzeville", -["Azeville"]="Azeville", -["Picauville"]="Picauville", -["Le_Molay"]="Le Molay", -["Longues_sur_Mer"]="Longues-sur-Mer", -["Carpiquet"]="Carpiquet", -["Bazenville"]="Bazenville", -["Sainte_Croix_sur_Mer"]="Sainte-Croix-sur-Mer", -["Beny_sur_Mer"]="Beny-sur-Mer", -["Rucqueville"]="Rucqueville", -["Sommervieu"]="Sommervieu", -["Lantheuil"]="Lantheuil", -["Evreux"]="Evreux", -["Chailey"]="Chailey", -["Needs_Oar_Point"]="Needs Oar Point", -["Funtington"]="Funtington", -["Tangmere"]="Tangmere", -["Ford"]="Ford", -} -function AIRBASE:Register(AirbaseName) -local self=BASE:Inherit(self,POSITIONABLE:New(AirbaseName)) -self.AirbaseName=AirbaseName -self.AirbaseZone=ZONE_RADIUS:New(AirbaseName,self:GetVec2(),8000) -return self -end -function AIRBASE:Find(DCSAirbase) -local AirbaseName=DCSAirbase:getName() -local AirbaseFound=_DATABASE:FindAirbase(AirbaseName) -return AirbaseFound -end -function AIRBASE:FindByName(AirbaseName) -local AirbaseFound=_DATABASE:FindAirbase(AirbaseName) -return AirbaseFound -end -function AIRBASE:GetDCSObject() -local DCSAirbase=Airbase.getByName(self.AirbaseName) -if DCSAirbase then -return DCSAirbase -end -return nil -end -function AIRBASE:GetZone() -return self.AirbaseZone -end -SCENERY={ -ClassName="SCENERY", -} -function SCENERY:Register(SceneryName,SceneryObject) -local self=BASE:Inherit(self,POSITIONABLE:New(SceneryName)) -self.SceneryName=SceneryName -self.SceneryObject=SceneryObject -return self -end -function SCENERY:GetDCSObject() -return self.SceneryObject -end -function SCENERY:GetThreatLevel() -return 0,"Scenery" -end -SCORING={ -ClassName="SCORING", -ClassID=0, -Players={}, -} -local _SCORINGCoalition= -{ -[1]="Red", -[2]="Blue", -} -local _SCORINGCategory= -{ -[Unit.Category.AIRPLANE]="Plane", -[Unit.Category.HELICOPTER]="Helicopter", -[Unit.Category.GROUND_UNIT]="Vehicle", -[Unit.Category.SHIP]="Ship", -[Unit.Category.STRUCTURE]="Structure", -} -function SCORING:New(GameName) -local self=BASE:Inherit(self,BASE:New()) -if GameName then -self.GameName=GameName -else -error("A game name must be given to register the scoring results") -end -self.ScoringObjects={} -self.ScoringZones={} -self:SetMessagesToAll() -self:SetMessagesHit(true) -self:SetMessagesDestroy(true) -self:SetMessagesScore(true) -self:SetMessagesZone(true) -self:SetScaleDestroyScore(10) -self:SetScaleDestroyPenalty(30) -self:SetFratricide(self.ScaleDestroyPenalty*3) -self:SetCoalitionChangePenalty(self.ScaleDestroyPenalty) -self:SetDisplayMessagePrefix() -self:HandleEvent(EVENTS.Dead,self._EventOnDeadOrCrash) -self:HandleEvent(EVENTS.Crash,self._EventOnDeadOrCrash) -self:HandleEvent(EVENTS.Hit,self._EventOnHit) -self:HandleEvent(EVENTS.PlayerEnterUnit) -self:HandleEvent(EVENTS.PlayerLeaveUnit) -self:OpenCSV(GameName) -return self -end -function SCORING:SetDisplayMessagePrefix(DisplayMessagePrefix) -self.DisplayMessagePrefix=DisplayMessagePrefix or"" -return self -end -function SCORING:SetScaleDestroyScore(Scale) -self.ScaleDestroyScore=Scale -return self -end -function SCORING:SetScaleDestroyPenalty(Scale) -self.ScaleDestroyPenalty=Scale -return self -end -function SCORING:AddUnitScore(ScoreUnit,Score) -local UnitName=ScoreUnit:GetName() -self.ScoringObjects[UnitName]=Score -return self -end -function SCORING:RemoveUnitScore(ScoreUnit) -local UnitName=ScoreUnit:GetName() -self.ScoringObjects[UnitName]=nil -return self -end -function SCORING:AddStaticScore(ScoreStatic,Score) -local StaticName=ScoreStatic:GetName() -self.ScoringObjects[StaticName]=Score -return self -end -function SCORING:RemoveStaticScore(ScoreStatic) -local StaticName=ScoreStatic:GetName() -self.ScoringObjects[StaticName]=nil -return self -end -function SCORING:AddScoreGroup(ScoreGroup,Score) -local ScoreUnits=ScoreGroup:GetUnits() -for ScoreUnitID,ScoreUnit in pairs(ScoreUnits)do -local UnitName=ScoreUnit:GetName() -self.ScoringObjects[UnitName]=Score -end -return self -end -function SCORING:AddZoneScore(ScoreZone,Score) -local ZoneName=ScoreZone:GetName() -self.ScoringZones[ZoneName]={} -self.ScoringZones[ZoneName].ScoreZone=ScoreZone -self.ScoringZones[ZoneName].Score=Score -return self -end -function SCORING:RemoveZoneScore(ScoreZone) -local ZoneName=ScoreZone:GetName() -self.ScoringZones[ZoneName]=nil -return self -end -function SCORING:SetMessagesHit(OnOff) -self.MessagesHit=OnOff -return self -end -function SCORING:IfMessagesHit() -return self.MessagesHit -end -function SCORING:SetMessagesDestroy(OnOff) -self.MessagesDestroy=OnOff -return self -end -function SCORING:IfMessagesDestroy() -return self.MessagesDestroy -end -function SCORING:SetMessagesScore(OnOff) -self.MessagesScore=OnOff -return self -end -function SCORING:IfMessagesScore() -return self.MessagesScore -end -function SCORING:SetMessagesZone(OnOff) -self.MessagesZone=OnOff -return self -end -function SCORING:IfMessagesZone() -return self.MessagesZone -end -function SCORING:SetMessagesToAll() -self.MessagesAudience=1 -return self -end -function SCORING:IfMessagesToAll() -return self.MessagesAudience==1 -end -function SCORING:SetMessagesToCoalition() -self.MessagesAudience=2 -return self -end -function SCORING:IfMessagesToCoalition() -return self.MessagesAudience==2 -end -function SCORING:SetFratricide(Fratricide) -self.Fratricide=Fratricide -return self -end -function SCORING:SetCoalitionChangePenalty(CoalitionChangePenalty) -self.CoalitionChangePenalty=CoalitionChangePenalty -return self -end -function SCORING:_AddPlayerFromUnit(UnitData) -self:F(UnitData) -if UnitData:IsAlive()then -local UnitName=UnitData:GetName() -local PlayerName=UnitData:GetPlayerName() -local UnitDesc=UnitData:GetDesc() -local UnitCategory=UnitDesc.category -local UnitCoalition=UnitData:GetCoalition() -local UnitTypeName=UnitData:GetTypeName() -local UnitThreatLevel,UnitThreatType=UnitData:GetThreatLevel() -self:T({PlayerName,UnitName,UnitCategory,UnitCoalition,UnitTypeName}) -if self.Players[PlayerName]==nil then -self.Players[PlayerName]={} -self.Players[PlayerName].Hit={} -self.Players[PlayerName].Destroy={} -self.Players[PlayerName].Goals={} -self.Players[PlayerName].Mission={} -self.Players[PlayerName].HitPlayers={} -self.Players[PlayerName].Score=0 -self.Players[PlayerName].Penalty=0 -self.Players[PlayerName].PenaltyCoalition=0 -self.Players[PlayerName].PenaltyWarning=0 -end -if not self.Players[PlayerName].UnitCoalition then -self.Players[PlayerName].UnitCoalition=UnitCoalition -else -if self.Players[PlayerName].UnitCoalition~=UnitCoalition then -self.Players[PlayerName].Penalty=self.Players[PlayerName].Penalty+50 -self.Players[PlayerName].PenaltyCoalition=self.Players[PlayerName].PenaltyCoalition+1 -MESSAGE:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."' changed coalition from ".._SCORINGCoalition[self.Players[PlayerName].UnitCoalition].." to ".._SCORINGCoalition[UnitCoalition].. -"(changed "..self.Players[PlayerName].PenaltyCoalition.." times the coalition). 50 Penalty points added.", -MESSAGE.Type.Information -):ToAll() -self:ScoreCSV(PlayerName,"","COALITION_PENALTY",1,-50,self.Players[PlayerName].UnitName,_SCORINGCoalition[self.Players[PlayerName].UnitCoalition],_SCORINGCategory[self.Players[PlayerName].UnitCategory],self.Players[PlayerName].UnitType, -UnitName,_SCORINGCoalition[UnitCoalition],_SCORINGCategory[UnitCategory],UnitData:GetTypeName()) -end -end -self.Players[PlayerName].UnitName=UnitName -self.Players[PlayerName].UnitCoalition=UnitCoalition -self.Players[PlayerName].UnitCategory=UnitCategory -self.Players[PlayerName].UnitType=UnitTypeName -self.Players[PlayerName].UNIT=UnitData -self.Players[PlayerName].ThreatLevel=UnitThreatLevel -self.Players[PlayerName].ThreatType=UnitThreatType -if self.Players[PlayerName].Penalty>self.Fratricide*0.50 then -if self.Players[PlayerName].PenaltyWarning<1 then -MESSAGE:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than "..self.Fratricide..", you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: "..self.Players[PlayerName].Penalty, -MESSAGE.Type.Information -):ToAll() -self.Players[PlayerName].PenaltyWarning=self.Players[PlayerName].PenaltyWarning+1 -end -end -if self.Players[PlayerName].Penalty>self.Fratricide then -MESSAGE:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", -MESSAGE.Type.Information -):ToAll() -UnitData:GetGroup():Destroy() -end -end -end -function SCORING:AddGoalScore(PlayerUnit,GoalTag,Text,Score) -local PlayerName=PlayerUnit:GetPlayerName() -self:E({PlayerUnit.UnitName,PlayerName,GoalTag,Text,Score}) -if PlayerName then -local PlayerData=self.Players[PlayerName] -PlayerData.Goals[GoalTag]=PlayerData.Goals[GoalTag]or{Score=0} -PlayerData.Goals[GoalTag].Score=PlayerData.Goals[GoalTag].Score+Score -PlayerData.Score=PlayerData.Score+Score -MESSAGE:NewType(self.DisplayMessagePrefix..Text,MESSAGE.Type.Information):ToAll() -self:ScoreCSV(PlayerName,"","GOAL_"..string.upper(GoalTag),1,Score,PlayerUnit:GetName()) -end -end -function SCORING:_AddMissionTaskScore(Mission,PlayerUnit,Text,Score) -local PlayerName=PlayerUnit:GetPlayerName() -local MissionName=Mission:GetName() -self:E({Mission:GetName(),PlayerUnit.UnitName,PlayerName,Text,Score}) -if PlayerName then -local PlayerData=self.Players[PlayerName] -if not PlayerData.Mission[MissionName]then -PlayerData.Mission[MissionName]={} -PlayerData.Mission[MissionName].ScoreTask=0 -PlayerData.Mission[MissionName].ScoreMission=0 -end -self:T(PlayerName) -self:T(PlayerData.Mission[MissionName]) -PlayerData.Score=self.Players[PlayerName].Score+Score -PlayerData.Mission[MissionName].ScoreTask=self.Players[PlayerName].Mission[MissionName].ScoreTask+Score -MESSAGE:NewType(self.DisplayMessagePrefix..MissionName.." : "..Text.." Score: "..Score,MESSAGE.Type.Information):ToAll() -self:ScoreCSV(PlayerName,"","TASK_"..MissionName:gsub(' ','_'),1,Score,PlayerUnit:GetName()) -end -end -function SCORING:_AddMissionScore(Mission,Text,Score) -local MissionName=Mission:GetName() -self:E({Mission,Text,Score}) -self:E(self.Players) -for PlayerName,PlayerData in pairs(self.Players)do -self:E(PlayerData) -if PlayerData.Mission[MissionName]then -PlayerData.Score=PlayerData.Score+Score -PlayerData.Mission[MissionName].ScoreMission=PlayerData.Mission[MissionName].ScoreMission+Score -MESSAGE:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."' has "..Text.." in Mission '"..MissionName.."'. ".. -Score.." mission score!", -MESSAGE.Type.Information):ToAll() -self:ScoreCSV(PlayerName,"","MISSION_"..MissionName:gsub(' ','_'),1,Score) -end -end -end -function SCORING:OnEventPlayerEnterUnit(Event) -if Event.IniUnit then -self:_AddPlayerFromUnit(Event.IniUnit) -local Menu=MENU_GROUP:New(Event.IniGroup,'Scoring') -local ReportGroupSummary=MENU_GROUP_COMMAND:New(Event.IniGroup,'Summary report players in group',Menu,SCORING.ReportScoreGroupSummary,self,Event.IniGroup) -local ReportGroupDetailed=MENU_GROUP_COMMAND:New(Event.IniGroup,'Detailed report players in group',Menu,SCORING.ReportScoreGroupDetailed,self,Event.IniGroup) -local ReportToAllSummary=MENU_GROUP_COMMAND:New(Event.IniGroup,'Summary report all players',Menu,SCORING.ReportScoreAllSummary,self,Event.IniGroup) -self:SetState(Event.IniUnit,"ScoringMenu",Menu) -end -end -function SCORING:OnEventPlayerLeaveUnit(Event) -if Event.IniUnit then -local Menu=self:GetState(Event.IniUnit,"ScoringMenu") -if Menu then -end -end -end -function SCORING:_EventOnHit(Event) -self:F({Event}) -local InitUnit=nil -local InitUNIT=nil -local InitUnitName="" -local InitGroup=nil -local InitGroupName="" -local InitPlayerName=nil -local InitCoalition=nil -local InitCategory=nil -local InitType=nil -local InitUnitCoalition=nil -local InitUnitCategory=nil -local InitUnitType=nil -local TargetUnit=nil -local TargetUNIT=nil -local TargetUnitName="" -local TargetGroup=nil -local TargetGroupName="" -local TargetPlayerName=nil -local TargetCoalition=nil -local TargetCategory=nil -local TargetType=nil -local TargetUnitCoalition=nil -local TargetUnitCategory=nil -local TargetUnitType=nil -if Event.IniDCSUnit then -InitUnit=Event.IniDCSUnit -InitUNIT=Event.IniUnit -InitUnitName=Event.IniDCSUnitName -InitGroup=Event.IniDCSGroup -InitGroupName=Event.IniDCSGroupName -InitPlayerName=Event.IniPlayerName -InitCoalition=Event.IniCoalition -InitCategory=Event.IniCategory -InitType=Event.IniTypeName -InitUnitCoalition=_SCORINGCoalition[InitCoalition] -InitUnitCategory=_SCORINGCategory[InitCategory] -InitUnitType=InitType -self:T({InitUnitName,InitGroupName,InitPlayerName,InitCoalition,InitCategory,InitType,InitUnitCoalition,InitUnitCategory,InitUnitType}) -end -if Event.TgtDCSUnit then -TargetUnit=Event.TgtDCSUnit -TargetUNIT=Event.TgtUnit -TargetUnitName=Event.TgtDCSUnitName -TargetGroup=Event.TgtDCSGroup -TargetGroupName=Event.TgtDCSGroupName -TargetPlayerName=Event.TgtPlayerName -TargetCoalition=Event.TgtCoalition -TargetCategory=Event.TgtCategory -TargetType=Event.TgtTypeName -TargetUnitCoalition=_SCORINGCoalition[TargetCoalition] -TargetUnitCategory=_SCORINGCategory[TargetCategory] -TargetUnitType=TargetType -self:T({TargetUnitName,TargetGroupName,TargetPlayerName,TargetCoalition,TargetCategory,TargetType,TargetUnitCoalition,TargetUnitCategory,TargetUnitType}) -end -if InitPlayerName~=nil then -self:_AddPlayerFromUnit(InitUNIT) -if self.Players[InitPlayerName]then -if TargetPlayerName~=nil then -self:_AddPlayerFromUnit(TargetUNIT) -end -self:T("Hitting Something") -if TargetCategory then -local Player=self.Players[InitPlayerName] -Player.Hit[TargetCategory]=Player.Hit[TargetCategory]or{} -Player.Hit[TargetCategory][TargetUnitName]=Player.Hit[TargetCategory][TargetUnitName]or{} -local PlayerHit=Player.Hit[TargetCategory][TargetUnitName] -PlayerHit.Score=PlayerHit.Score or 0 -PlayerHit.Penalty=PlayerHit.Penalty or 0 -PlayerHit.ScoreHit=PlayerHit.ScoreHit or 0 -PlayerHit.PenaltyHit=PlayerHit.PenaltyHit or 0 -PlayerHit.TimeStamp=PlayerHit.TimeStamp or 0 -PlayerHit.UNIT=PlayerHit.UNIT or TargetUNIT -PlayerHit.ThreatLevel,PlayerHit.ThreatType=PlayerHit.UNIT:GetThreatLevel() -if timer.getTime()-PlayerHit.TimeStamp>1 then -PlayerHit.TimeStamp=timer.getTime() -if TargetPlayerName~=nil then -Player.HitPlayers[TargetPlayerName]=true -end -local Score=0 -if InitCoalition then -if InitCoalition==TargetCoalition then -Player.Penalty=Player.Penalty+10 -PlayerHit.Penalty=PlayerHit.Penalty+10 -PlayerHit.PenaltyHit=PlayerHit.PenaltyHit+1 -if TargetPlayerName~=nil then -MESSAGE -:NewType(self.DisplayMessagePrefix.."Player '"..InitPlayerName.."' hit friendly player '"..TargetPlayerName.."' ".. -TargetUnitCategory.." ( "..TargetType.." ) "..PlayerHit.PenaltyHit.." times. ".. -"Penalty: -"..PlayerHit.Penalty..". Score Total:"..Player.Score-Player.Penalty, -MESSAGE.Type.Update -) -:ToAllIf(self:IfMessagesHit()and self:IfMessagesToAll()) -:ToCoalitionIf(InitCoalition,self:IfMessagesHit()and self:IfMessagesToCoalition()) -else -MESSAGE -:NewType(self.DisplayMessagePrefix.."Player '"..InitPlayerName.."' hit friendly target ".. -TargetUnitCategory.." ( "..TargetType.." ) "..PlayerHit.PenaltyHit.." times. ".. -"Penalty: -"..PlayerHit.Penalty..". Score Total:"..Player.Score-Player.Penalty, -MESSAGE.Type.Update -) -:ToAllIf(self:IfMessagesHit()and self:IfMessagesToAll()) -:ToCoalitionIf(InitCoalition,self:IfMessagesHit()and self:IfMessagesToCoalition()) -end -self:ScoreCSV(InitPlayerName,TargetPlayerName,"HIT_PENALTY",1,-10,InitUnitName,InitUnitCoalition,InitUnitCategory,InitUnitType,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) -else -Player.Score=Player.Score+1 -PlayerHit.Score=PlayerHit.Score+1 -PlayerHit.ScoreHit=PlayerHit.ScoreHit+1 -if TargetPlayerName~=nil then -MESSAGE -:NewType(self.DisplayMessagePrefix.."Player '"..InitPlayerName.."' hit enemy player '"..TargetPlayerName.."' ".. -TargetUnitCategory.." ( "..TargetType.." ) "..PlayerHit.ScoreHit.." times. ".. -"Score: "..PlayerHit.Score..". Score Total:"..Player.Score-Player.Penalty, -MESSAGE.Type.Update -) -:ToAllIf(self:IfMessagesHit()and self:IfMessagesToAll()) -:ToCoalitionIf(InitCoalition,self:IfMessagesHit()and self:IfMessagesToCoalition()) -else -MESSAGE -:NewType(self.DisplayMessagePrefix.."Player '"..InitPlayerName.."' hit enemy target ".. -TargetUnitCategory.." ( "..TargetType.." ) "..PlayerHit.ScoreHit.." times. ".. -"Score: "..PlayerHit.Score..". Score Total:"..Player.Score-Player.Penalty, -MESSAGE.Type.Update -) -:ToAllIf(self:IfMessagesHit()and self:IfMessagesToAll()) -:ToCoalitionIf(InitCoalition,self:IfMessagesHit()and self:IfMessagesToCoalition()) -end -self:ScoreCSV(InitPlayerName,TargetPlayerName,"HIT_SCORE",1,1,InitUnitName,InitUnitCoalition,InitUnitCategory,InitUnitType,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) -end -else -MESSAGE -:NewType(self.DisplayMessagePrefix.."Player '"..InitPlayerName.."' hit scenery object.", -MESSAGE.Type.Update -) -:ToAllIf(self:IfMessagesHit()and self:IfMessagesToAll()) -:ToCoalitionIf(InitCoalition,self:IfMessagesHit()and self:IfMessagesToCoalition()) -self:ScoreCSV(InitPlayerName,"","HIT_SCORE",1,0,InitUnitName,InitUnitCoalition,InitUnitCategory,InitUnitType,TargetUnitName,"","Scenery",TargetUnitType) -end -end -end -end -elseif InitPlayerName==nil then -end -if Event.WeaponPlayerName~=nil then -self:_AddPlayerFromUnit(Event.WeaponUNIT) -if self.Players[Event.WeaponPlayerName]then -if TargetPlayerName~=nil then -self:_AddPlayerFromUnit(TargetUNIT) -end -self:T("Hitting Scenery") -if TargetCategory then -local Player=self.Players[Event.WeaponPlayerName] -Player.Hit[TargetCategory]=Player.Hit[TargetCategory]or{} -Player.Hit[TargetCategory][TargetUnitName]=Player.Hit[TargetCategory][TargetUnitName]or{} -local PlayerHit=Player.Hit[TargetCategory][TargetUnitName] -PlayerHit.Score=PlayerHit.Score or 0 -PlayerHit.Penalty=PlayerHit.Penalty or 0 -PlayerHit.ScoreHit=PlayerHit.ScoreHit or 0 -PlayerHit.PenaltyHit=PlayerHit.PenaltyHit or 0 -PlayerHit.TimeStamp=PlayerHit.TimeStamp or 0 -PlayerHit.UNIT=PlayerHit.UNIT or TargetUNIT -PlayerHit.ThreatLevel,PlayerHit.ThreatType=PlayerHit.UNIT:GetThreatLevel() -if timer.getTime()-PlayerHit.TimeStamp>1 then -PlayerHit.TimeStamp=timer.getTime() -local Score=0 -if InitCoalition then -if InitCoalition==TargetCoalition then -Player.Penalty=Player.Penalty+10 -PlayerHit.Penalty=PlayerHit.Penalty+10 -PlayerHit.PenaltyHit=PlayerHit.PenaltyHit+1 -MESSAGE -:NewType(self.DisplayMessagePrefix.."Player '"..Event.WeaponPlayerName.."' hit friendly target ".. -TargetUnitCategory.." ( "..TargetType.." ) ".. -"Penalty: -"..PlayerHit.Penalty.." = "..Player.Score-Player.Penalty, -MESSAGE.Type.Update -) -:ToAllIf(self:IfMessagesHit()and self:IfMessagesToAll()) -:ToCoalitionIf(Event.WeaponCoalition,self:IfMessagesHit()and self:IfMessagesToCoalition()) -self:ScoreCSV(Event.WeaponPlayerName,TargetPlayerName,"HIT_PENALTY",1,-10,Event.WeaponName,Event.WeaponCoalition,Event.WeaponCategory,Event.WeaponTypeName,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) -else -Player.Score=Player.Score+1 -PlayerHit.Score=PlayerHit.Score+1 -PlayerHit.ScoreHit=PlayerHit.ScoreHit+1 -MESSAGE -:NewType(self.DisplayMessagePrefix.."Player '"..Event.WeaponPlayerName.."' hit enemy target ".. -TargetUnitCategory.." ( "..TargetType.." ) ".. -"Score: +"..PlayerHit.Score.." = "..Player.Score-Player.Penalty, -MESSAGE.Type.Update -) -:ToAllIf(self:IfMessagesHit()and self:IfMessagesToAll()) -:ToCoalitionIf(Event.WeaponCoalition,self:IfMessagesHit()and self:IfMessagesToCoalition()) -self:ScoreCSV(Event.WeaponPlayerName,TargetPlayerName,"HIT_SCORE",1,1,Event.WeaponName,Event.WeaponCoalition,Event.WeaponCategory,Event.WeaponTypeName,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) -end -else -MESSAGE -:NewType(self.DisplayMessagePrefix.."Player '"..Event.WeaponPlayerName.."' hit scenery object.", -MESSAGE.Type.Update -) -:ToAllIf(self:IfMessagesHit()and self:IfMessagesToAll()) -:ToCoalitionIf(InitCoalition,self:IfMessagesHit()and self:IfMessagesToCoalition()) -self:ScoreCSV(Event.WeaponPlayerName,"","HIT_SCORE",1,0,Event.WeaponName,Event.WeaponCoalition,Event.WeaponCategory,Event.WeaponTypeName,TargetUnitName,"","Scenery",TargetUnitType) -end -end -end -end -end -end -function SCORING:_EventOnDeadOrCrash(Event) -self:F({Event}) -local TargetUnit=nil -local TargetGroup=nil -local TargetUnitName="" -local TargetGroupName="" -local TargetPlayerName="" -local TargetCoalition=nil -local TargetCategory=nil -local TargetType=nil -local TargetUnitCoalition=nil -local TargetUnitCategory=nil -local TargetUnitType=nil -if Event.IniDCSUnit then -TargetUnit=Event.IniUnit -TargetUnitName=Event.IniDCSUnitName -TargetGroup=Event.IniDCSGroup -TargetGroupName=Event.IniDCSGroupName -TargetPlayerName=Event.IniPlayerName -TargetCoalition=Event.IniCoalition -TargetCategory=Event.IniCategory -TargetType=Event.IniTypeName -TargetUnitCoalition=_SCORINGCoalition[TargetCoalition] -TargetUnitCategory=_SCORINGCategory[TargetCategory] -TargetUnitType=TargetType -self:T({TargetUnitName,TargetGroupName,TargetPlayerName,TargetCoalition,TargetCategory,TargetType}) -end -for PlayerName,Player in pairs(self.Players)do -if Player then -self:T("Something got destroyed") -local InitUnitName=Player.UnitName -local InitUnitType=Player.UnitType -local InitCoalition=Player.UnitCoalition -local InitCategory=Player.UnitCategory -local InitUnitCoalition=_SCORINGCoalition[InitCoalition] -local InitUnitCategory=_SCORINGCategory[InitCategory] -self:T({InitUnitName,InitUnitType,InitUnitCoalition,InitCoalition,InitUnitCategory,InitCategory}) -local Destroyed=false -if Player and Player.Hit and Player.Hit[TargetCategory]and Player.Hit[TargetCategory][TargetUnitName]and Player.Hit[TargetCategory][TargetUnitName].TimeStamp~=0 then -local TargetThreatLevel=Player.Hit[TargetCategory][TargetUnitName].ThreatLevel -local TargetThreatType=Player.Hit[TargetCategory][TargetUnitName].ThreatType -Player.Destroy[TargetCategory]=Player.Destroy[TargetCategory]or{} -Player.Destroy[TargetCategory][TargetType]=Player.Destroy[TargetCategory][TargetType]or{} -local TargetDestroy=Player.Destroy[TargetCategory][TargetType] -TargetDestroy.Score=TargetDestroy.Score or 0 -TargetDestroy.ScoreDestroy=TargetDestroy.ScoreDestroy or 0 -TargetDestroy.Penalty=TargetDestroy.Penalty or 0 -TargetDestroy.PenaltyDestroy=TargetDestroy.PenaltyDestroy or 0 -if TargetCoalition then -if InitCoalition==TargetCoalition then -local ThreatLevelTarget=TargetThreatLevel -local ThreatTypeTarget=TargetThreatType -local ThreatLevelPlayer=Player.ThreatLevel/10+1 -local ThreatPenalty=math.ceil((ThreatLevelTarget/ThreatLevelPlayer)*self.ScaleDestroyPenalty/10) -self:E({ThreatLevel=ThreatPenalty,ThreatLevelTarget=ThreatLevelTarget,ThreatTypeTarget=ThreatTypeTarget,ThreatLevelPlayer=ThreatLevelPlayer}) -Player.Penalty=Player.Penalty+ThreatPenalty -TargetDestroy.Penalty=TargetDestroy.Penalty+ThreatPenalty -TargetDestroy.PenaltyDestroy=TargetDestroy.PenaltyDestroy+1 -if Player.HitPlayers[TargetPlayerName]then -MESSAGE -:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."' destroyed friendly player '"..TargetPlayerName.."' ".. -TargetUnitCategory.." ( "..ThreatTypeTarget.." ) ".. -"Penalty: -"..TargetDestroy.Penalty.." = "..Player.Score-Player.Penalty, -MESSAGE.Type.Information -) -:ToAllIf(self:IfMessagesDestroy()and self:IfMessagesToAll()) -:ToCoalitionIf(InitCoalition,self:IfMessagesDestroy()and self:IfMessagesToCoalition()) -else -MESSAGE -:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."' destroyed friendly target ".. -TargetUnitCategory.." ( "..ThreatTypeTarget.." ) ".. -"Penalty: -"..TargetDestroy.Penalty.." = "..Player.Score-Player.Penalty, -MESSAGE.Type.Information -) -:ToAllIf(self:IfMessagesDestroy()and self:IfMessagesToAll()) -:ToCoalitionIf(InitCoalition,self:IfMessagesDestroy()and self:IfMessagesToCoalition()) -end -Destroyed=true -self:ScoreCSV(PlayerName,TargetPlayerName,"DESTROY_PENALTY",1,ThreatPenalty,InitUnitName,InitUnitCoalition,InitUnitCategory,InitUnitType,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) -else -local ThreatLevelTarget=TargetThreatLevel -local ThreatTypeTarget=TargetThreatType -local ThreatLevelPlayer=Player.ThreatLevel/10+1 -local ThreatScore=math.ceil((ThreatLevelTarget/ThreatLevelPlayer)*self.ScaleDestroyScore/10) -self:E({ThreatLevel=ThreatScore,ThreatLevelTarget=ThreatLevelTarget,ThreatTypeTarget=ThreatTypeTarget,ThreatLevelPlayer=ThreatLevelPlayer}) -Player.Score=Player.Score+ThreatScore -TargetDestroy.Score=TargetDestroy.Score+ThreatScore -TargetDestroy.ScoreDestroy=TargetDestroy.ScoreDestroy+1 -if Player.HitPlayers[TargetPlayerName]then -MESSAGE -:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."' destroyed enemy player '"..TargetPlayerName.."' ".. -TargetUnitCategory.." ( "..ThreatTypeTarget.." ) ".. -"Score: +"..TargetDestroy.Score.." = "..Player.Score-Player.Penalty, -MESSAGE.Type.Information -) -:ToAllIf(self:IfMessagesDestroy()and self:IfMessagesToAll()) -:ToCoalitionIf(InitCoalition,self:IfMessagesDestroy()and self:IfMessagesToCoalition()) -else -MESSAGE -:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."' destroyed enemy ".. -TargetUnitCategory.." ( "..ThreatTypeTarget.." ) ".. -"Score: +"..TargetDestroy.Score.." = "..Player.Score-Player.Penalty, -MESSAGE.Type.Information -) -:ToAllIf(self:IfMessagesDestroy()and self:IfMessagesToAll()) -:ToCoalitionIf(InitCoalition,self:IfMessagesDestroy()and self:IfMessagesToCoalition()) -end -Destroyed=true -self:ScoreCSV(PlayerName,TargetPlayerName,"DESTROY_SCORE",1,ThreatScore,InitUnitName,InitUnitCoalition,InitUnitCategory,InitUnitType,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) -local UnitName=TargetUnit:GetName() -local Score=self.ScoringObjects[UnitName] -if Score then -Player.Score=Player.Score+Score -TargetDestroy.Score=TargetDestroy.Score+Score -MESSAGE -:NewType(self.DisplayMessagePrefix.."Special target '"..TargetUnitCategory.." ( "..ThreatTypeTarget.." ) ".." destroyed! ".. -"Player '"..PlayerName.."' receives an extra "..Score.." points! Total: "..Player.Score-Player.Penalty, -MESSAGE.Type.Information -) -:ToAllIf(self:IfMessagesScore()and self:IfMessagesToAll()) -:ToCoalitionIf(InitCoalition,self:IfMessagesScore()and self:IfMessagesToCoalition()) -self:ScoreCSV(PlayerName,TargetPlayerName,"DESTROY_SCORE",1,Score,InitUnitName,InitUnitCoalition,InitUnitCategory,InitUnitType,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) -Destroyed=true -end -for ZoneName,ScoreZoneData in pairs(self.ScoringZones)do -self:E({ScoringZone=ScoreZoneData}) -local ScoreZone=ScoreZoneData.ScoreZone -local Score=ScoreZoneData.Score -if ScoreZone:IsVec2InZone(TargetUnit:GetVec2())then -Player.Score=Player.Score+Score -TargetDestroy.Score=TargetDestroy.Score+Score -MESSAGE -:NewType(self.DisplayMessagePrefix.."Target destroyed in zone '"..ScoreZone:GetName().."'.".. -"Player '"..PlayerName.."' receives an extra "..Score.." points! ".. -"Total: "..Player.Score-Player.Penalty, -MESSAGE.Type.Information) -:ToAllIf(self:IfMessagesZone()and self:IfMessagesToAll()) -:ToCoalitionIf(InitCoalition,self:IfMessagesZone()and self:IfMessagesToCoalition()) -self:ScoreCSV(PlayerName,TargetPlayerName,"DESTROY_SCORE",1,Score,InitUnitName,InitUnitCoalition,InitUnitCategory,InitUnitType,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) -Destroyed=true -end -end -end -else -for ZoneName,ScoreZoneData in pairs(self.ScoringZones)do -self:E({ScoringZone=ScoreZoneData}) -local ScoreZone=ScoreZoneData.ScoreZone -local Score=ScoreZoneData.Score -if ScoreZone:IsVec2InZone(TargetUnit:GetVec2())then -Player.Score=Player.Score+Score -TargetDestroy.Score=TargetDestroy.Score+Score -MESSAGE -:NewType(self.DisplayMessagePrefix.."Scenery destroyed in zone '"..ScoreZone:GetName().."'.".. -"Player '"..PlayerName.."' receives an extra "..Score.." points! ".. -"Total: "..Player.Score-Player.Penalty, -MESSAGE.Type.Information -) -:ToAllIf(self:IfMessagesZone()and self:IfMessagesToAll()) -:ToCoalitionIf(InitCoalition,self:IfMessagesZone()and self:IfMessagesToCoalition()) -Destroyed=true -self:ScoreCSV(PlayerName,"","DESTROY_SCORE",1,Score,InitUnitName,InitUnitCoalition,InitUnitCategory,InitUnitType,TargetUnitName,"","Scenery",TargetUnitType) -end -end -end -if Destroyed then -Player.Hit[TargetCategory][TargetUnitName].TimeStamp=0 -end -end -end -end -end -function SCORING:ReportDetailedPlayerHits(PlayerName) -local ScoreMessage="" -local PlayerScore=0 -local PlayerPenalty=0 -local PlayerData=self.Players[PlayerName] -if PlayerData then -self:T("Score Player: "..PlayerName) -local InitUnitCoalition=_SCORINGCoalition[PlayerData.UnitCoalition] -local InitUnitCategory=_SCORINGCategory[PlayerData.UnitCategory] -local InitUnitType=PlayerData.UnitType -local InitUnitName=PlayerData.UnitName -local ScoreMessageHits="" -for CategoryID,CategoryName in pairs(_SCORINGCategory)do -self:T(CategoryName) -if PlayerData.Hit[CategoryID]then -self:T("Hit scores exist for player "..PlayerName) -local Score=0 -local ScoreHit=0 -local Penalty=0 -local PenaltyHit=0 -for UnitName,UnitData in pairs(PlayerData.Hit[CategoryID])do -Score=Score+UnitData.Score -ScoreHit=ScoreHit+UnitData.ScoreHit -Penalty=Penalty+UnitData.Penalty -PenaltyHit=UnitData.PenaltyHit -end -local ScoreMessageHit=string.format("%s:%d ",CategoryName,Score-Penalty) -self:T(ScoreMessageHit) -ScoreMessageHits=ScoreMessageHits..ScoreMessageHit -PlayerScore=PlayerScore+Score -PlayerPenalty=PlayerPenalty+Penalty -else -end -end -if ScoreMessageHits~=""then -ScoreMessage="Hits: "..ScoreMessageHits -end -end -return ScoreMessage,PlayerScore,PlayerPenalty -end -function SCORING:ReportDetailedPlayerDestroys(PlayerName) -local ScoreMessage="" -local PlayerScore=0 -local PlayerPenalty=0 -local PlayerData=self.Players[PlayerName] -if PlayerData then -self:T("Score Player: "..PlayerName) -local InitUnitCoalition=_SCORINGCoalition[PlayerData.UnitCoalition] -local InitUnitCategory=_SCORINGCategory[PlayerData.UnitCategory] -local InitUnitType=PlayerData.UnitType -local InitUnitName=PlayerData.UnitName -local ScoreMessageDestroys="" -for CategoryID,CategoryName in pairs(_SCORINGCategory)do -if PlayerData.Destroy[CategoryID]then -self:T("Destroy scores exist for player "..PlayerName) -local Score=0 -local ScoreDestroy=0 -local Penalty=0 -local PenaltyDestroy=0 -for UnitName,UnitData in pairs(PlayerData.Destroy[CategoryID])do -self:E({UnitData=UnitData}) -if UnitData~={}then -Score=Score+UnitData.Score -ScoreDestroy=ScoreDestroy+UnitData.ScoreDestroy -Penalty=Penalty+UnitData.Penalty -PenaltyDestroy=PenaltyDestroy+UnitData.PenaltyDestroy -end -end -local ScoreMessageDestroy=string.format(" %s:%d ",CategoryName,Score-Penalty) -self:T(ScoreMessageDestroy) -ScoreMessageDestroys=ScoreMessageDestroys..ScoreMessageDestroy -PlayerScore=PlayerScore+Score -PlayerPenalty=PlayerPenalty+Penalty -else -end -end -if ScoreMessageDestroys~=""then -ScoreMessage="Destroys: "..ScoreMessageDestroys -end -end -return ScoreMessage,PlayerScore,PlayerPenalty -end -function SCORING:ReportDetailedPlayerCoalitionChanges(PlayerName) -local ScoreMessage="" -local PlayerScore=0 -local PlayerPenalty=0 -local PlayerData=self.Players[PlayerName] -if PlayerData then -self:T("Score Player: "..PlayerName) -local InitUnitCoalition=_SCORINGCoalition[PlayerData.UnitCoalition] -local InitUnitCategory=_SCORINGCategory[PlayerData.UnitCategory] -local InitUnitType=PlayerData.UnitType -local InitUnitName=PlayerData.UnitName -local ScoreMessageCoalitionChangePenalties="" -if PlayerData.PenaltyCoalition~=0 then -ScoreMessageCoalitionChangePenalties=ScoreMessageCoalitionChangePenalties..string.format(" -%d (%d changed)",PlayerData.Penalty,PlayerData.PenaltyCoalition) -PlayerPenalty=PlayerPenalty+PlayerData.Penalty -end -if ScoreMessageCoalitionChangePenalties~=""then -ScoreMessage=ScoreMessage.."Coalition Penalties: "..ScoreMessageCoalitionChangePenalties -end -end -return ScoreMessage,PlayerScore,PlayerPenalty -end -function SCORING:ReportDetailedPlayerGoals(PlayerName) -local ScoreMessage="" -local PlayerScore=0 -local PlayerPenalty=0 -local PlayerData=self.Players[PlayerName] -if PlayerData then -self:T("Score Player: "..PlayerName) -local InitUnitCoalition=_SCORINGCoalition[PlayerData.UnitCoalition] -local InitUnitCategory=_SCORINGCategory[PlayerData.UnitCategory] -local InitUnitType=PlayerData.UnitType -local InitUnitName=PlayerData.UnitName -local ScoreMessageGoal="" -local ScoreGoal=0 -local ScoreTask=0 -for GoalName,GoalData in pairs(PlayerData.Goals)do -ScoreGoal=ScoreGoal+GoalData.Score -ScoreMessageGoal=ScoreMessageGoal.."'"..GoalName.."':"..GoalData.Score.."; " -end -PlayerScore=PlayerScore+ScoreGoal -if ScoreMessageGoal~=""then -ScoreMessage="Goals: "..ScoreMessageGoal -end -end -return ScoreMessage,PlayerScore,PlayerPenalty -end -function SCORING:ReportDetailedPlayerMissions(PlayerName) -local ScoreMessage="" -local PlayerScore=0 -local PlayerPenalty=0 -local PlayerData=self.Players[PlayerName] -if PlayerData then -self:T("Score Player: "..PlayerName) -local InitUnitCoalition=_SCORINGCoalition[PlayerData.UnitCoalition] -local InitUnitCategory=_SCORINGCategory[PlayerData.UnitCategory] -local InitUnitType=PlayerData.UnitType -local InitUnitName=PlayerData.UnitName -local ScoreMessageMission="" -local ScoreMission=0 -local ScoreTask=0 -for MissionName,MissionData in pairs(PlayerData.Mission)do -ScoreMission=ScoreMission+MissionData.ScoreMission -ScoreTask=ScoreTask+MissionData.ScoreTask -ScoreMessageMission=ScoreMessageMission.."'"..MissionName.."'; " -end -PlayerScore=PlayerScore+ScoreMission+ScoreTask -if ScoreMessageMission~=""then -ScoreMessage="Tasks: "..ScoreTask.." Mission: "..ScoreMission.." ( "..ScoreMessageMission..")" -end -end -return ScoreMessage,PlayerScore,PlayerPenalty -end -function SCORING:ReportScoreGroupSummary(PlayerGroup) -local PlayerMessage="" -self:T("Report Score Group Summary") -local PlayerUnits=PlayerGroup:GetUnits() -for UnitID,PlayerUnit in pairs(PlayerUnits)do -local PlayerUnit=PlayerUnit -local PlayerName=PlayerUnit:GetPlayerName() -if PlayerName then -local ReportHits,ScoreHits,PenaltyHits=self:ReportDetailedPlayerHits(PlayerName) -ReportHits=ReportHits~=""and"\n- "..ReportHits or ReportHits -self:E({ReportHits,ScoreHits,PenaltyHits}) -local ReportDestroys,ScoreDestroys,PenaltyDestroys=self:ReportDetailedPlayerDestroys(PlayerName) -ReportDestroys=ReportDestroys~=""and"\n- "..ReportDestroys or ReportDestroys -self:E({ReportDestroys,ScoreDestroys,PenaltyDestroys}) -local ReportCoalitionChanges,ScoreCoalitionChanges,PenaltyCoalitionChanges=self:ReportDetailedPlayerCoalitionChanges(PlayerName) -ReportCoalitionChanges=ReportCoalitionChanges~=""and"\n- "..ReportCoalitionChanges or ReportCoalitionChanges -self:E({ReportCoalitionChanges,ScoreCoalitionChanges,PenaltyCoalitionChanges}) -local ReportGoals,ScoreGoals,PenaltyGoals=self:ReportDetailedPlayerGoals(PlayerName) -ReportGoals=ReportGoals~=""and"\n- "..ReportGoals or ReportGoals -self:E({ReportGoals,ScoreGoals,PenaltyGoals}) -local ReportMissions,ScoreMissions,PenaltyMissions=self:ReportDetailedPlayerMissions(PlayerName) -ReportMissions=ReportMissions~=""and"\n- "..ReportMissions or ReportMissions -self:E({ReportMissions,ScoreMissions,PenaltyMissions}) -local PlayerScore=ScoreHits+ScoreDestroys+ScoreCoalitionChanges+ScoreGoals+ScoreMissions -local PlayerPenalty=PenaltyHits+PenaltyDestroys+PenaltyCoalitionChanges+ScoreGoals+PenaltyMissions -PlayerMessage= -string.format("Player '%s' Score = %d ( %d Score, -%d Penalties )", -PlayerName, -PlayerScore-PlayerPenalty, -PlayerScore, -PlayerPenalty -) -MESSAGE:NewType(PlayerMessage,MESSAGE.Type.Detailed):ToGroup(PlayerGroup) -end -end -end -function SCORING:ReportScoreGroupDetailed(PlayerGroup) -local PlayerMessage="" -self:T("Report Score Group Detailed") -local PlayerUnits=PlayerGroup:GetUnits() -for UnitID,PlayerUnit in pairs(PlayerUnits)do -local PlayerUnit=PlayerUnit -local PlayerName=PlayerUnit:GetPlayerName() -if PlayerName then -local ReportHits,ScoreHits,PenaltyHits=self:ReportDetailedPlayerHits(PlayerName) -ReportHits=ReportHits~=""and"\n- "..ReportHits or ReportHits -self:E({ReportHits,ScoreHits,PenaltyHits}) -local ReportDestroys,ScoreDestroys,PenaltyDestroys=self:ReportDetailedPlayerDestroys(PlayerName) -ReportDestroys=ReportDestroys~=""and"\n- "..ReportDestroys or ReportDestroys -self:E({ReportDestroys,ScoreDestroys,PenaltyDestroys}) -local ReportCoalitionChanges,ScoreCoalitionChanges,PenaltyCoalitionChanges=self:ReportDetailedPlayerCoalitionChanges(PlayerName) -ReportCoalitionChanges=ReportCoalitionChanges~=""and"\n- "..ReportCoalitionChanges or ReportCoalitionChanges -self:E({ReportCoalitionChanges,ScoreCoalitionChanges,PenaltyCoalitionChanges}) -local ReportGoals,ScoreGoals,PenaltyGoals=self:ReportDetailedPlayerGoals(PlayerName) -ReportGoals=ReportGoals~=""and"\n- "..ReportGoals or ReportGoals -self:E({ReportGoals,ScoreGoals,PenaltyGoals}) -local ReportMissions,ScoreMissions,PenaltyMissions=self:ReportDetailedPlayerMissions(PlayerName) -ReportMissions=ReportMissions~=""and"\n- "..ReportMissions or ReportMissions -self:E({ReportMissions,ScoreMissions,PenaltyMissions}) -local PlayerScore=ScoreHits+ScoreDestroys+ScoreCoalitionChanges+ScoreGoals+ScoreMissions -local PlayerPenalty=PenaltyHits+PenaltyDestroys+PenaltyCoalitionChanges+ScoreGoals+PenaltyMissions -PlayerMessage= -string.format("Player '%s' Score = %d ( %d Score, -%d Penalties )%s%s%s%s%s", -PlayerName, -PlayerScore-PlayerPenalty, -PlayerScore, -PlayerPenalty, -ReportHits, -ReportDestroys, -ReportCoalitionChanges, -ReportGoals, -ReportMissions -) -MESSAGE:NewType(PlayerMessage,MESSAGE.Type.Detailed):ToGroup(PlayerGroup) -end -end -end -function SCORING:ReportScoreAllSummary(PlayerGroup) -local PlayerMessage="" -self:T("Report Score All Players") -for PlayerName,PlayerData in pairs(self.Players)do -if PlayerName then -local ReportHits,ScoreHits,PenaltyHits=self:ReportDetailedPlayerHits(PlayerName) -ReportHits=ReportHits~=""and"\n- "..ReportHits or ReportHits -self:E({ReportHits,ScoreHits,PenaltyHits}) -local ReportDestroys,ScoreDestroys,PenaltyDestroys=self:ReportDetailedPlayerDestroys(PlayerName) -ReportDestroys=ReportDestroys~=""and"\n- "..ReportDestroys or ReportDestroys -self:E({ReportDestroys,ScoreDestroys,PenaltyDestroys}) -local ReportCoalitionChanges,ScoreCoalitionChanges,PenaltyCoalitionChanges=self:ReportDetailedPlayerCoalitionChanges(PlayerName) -ReportCoalitionChanges=ReportCoalitionChanges~=""and"\n- "..ReportCoalitionChanges or ReportCoalitionChanges -self:E({ReportCoalitionChanges,ScoreCoalitionChanges,PenaltyCoalitionChanges}) -local ReportGoals,ScoreGoals,PenaltyGoals=self:ReportDetailedPlayerGoals(PlayerName) -ReportGoals=ReportGoals~=""and"\n- "..ReportGoals or ReportGoals -self:E({ReportGoals,ScoreGoals,PenaltyGoals}) -local ReportMissions,ScoreMissions,PenaltyMissions=self:ReportDetailedPlayerMissions(PlayerName) -ReportMissions=ReportMissions~=""and"\n- "..ReportMissions or ReportMissions -self:E({ReportMissions,ScoreMissions,PenaltyMissions}) -local PlayerScore=ScoreHits+ScoreDestroys+ScoreCoalitionChanges+ScoreGoals+ScoreMissions -local PlayerPenalty=PenaltyHits+PenaltyDestroys+PenaltyCoalitionChanges+ScoreGoals+PenaltyMissions -PlayerMessage= -string.format("Player '%s' Score = %d ( %d Score, -%d Penalties )", -PlayerName, -PlayerScore-PlayerPenalty, -PlayerScore, -PlayerPenalty -) -MESSAGE:NewType(PlayerMessage,MESSAGE.Type.Overview):ToGroup(PlayerGroup) -end -end -end -function SCORING:SecondsToClock(sSeconds) -local nSeconds=sSeconds -if nSeconds==0 then -return"00:00:00"; -else -nHours=string.format("%02.f",math.floor(nSeconds/3600)); -nMins=string.format("%02.f",math.floor(nSeconds/60-(nHours*60))); -nSecs=string.format("%02.f",math.floor(nSeconds-nHours*3600-nMins*60)); -return nHours..":"..nMins..":"..nSecs -end -end -function SCORING:OpenCSV(ScoringCSV) -self:F(ScoringCSV) -if lfs and io and os then -if ScoringCSV then -self.ScoringCSV=ScoringCSV -local fdir=lfs.writedir()..[[Logs\]]..self.ScoringCSV.." "..os.date("%Y-%m-%d %H-%M-%S")..".csv" -self.CSVFile,self.err=io.open(fdir,"w+") -if not self.CSVFile then -error("Error: Cannot open CSV file in "..lfs.writedir()) -end -self.CSVFile:write('"GameName","RunTime","Time","PlayerName","TargetPlayerName","ScoreType","PlayerUnitCoaltion","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n') -self.RunTime=os.date("%y-%m-%d_%H-%M-%S") -else -error("A string containing the CSV file name must be given.") -end -else -self:E("The MissionScripting.lua file has not been changed to allow lfs, io and os modules to be used...") -end -return self -end -function SCORING:ScoreCSV(PlayerName,TargetPlayerName,ScoreType,ScoreTimes,ScoreAmount,PlayerUnitName,PlayerUnitCoalition,PlayerUnitCategory,PlayerUnitType,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) -local ScoreTime=self:SecondsToClock(timer.getTime()) -PlayerName=PlayerName:gsub('"','_') -TargetPlayerName=TargetPlayerName or"" -TargetPlayerName=TargetPlayerName:gsub('"','_') -if PlayerUnitName and PlayerUnitName~=''then -local PlayerUnit=Unit.getByName(PlayerUnitName) -if PlayerUnit then -if not PlayerUnitCategory then -PlayerUnitCategory=_SCORINGCategory[PlayerUnit:getDesc().category] -end -if not PlayerUnitCoalition then -PlayerUnitCoalition=_SCORINGCoalition[PlayerUnit:getCoalition()] -end -if not PlayerUnitType then -PlayerUnitType=PlayerUnit:getTypeName() -end -else -PlayerUnitName='' -PlayerUnitCategory='' -PlayerUnitCoalition='' -PlayerUnitType='' -end -else -PlayerUnitName='' -PlayerUnitCategory='' -PlayerUnitCoalition='' -PlayerUnitType='' -end -TargetUnitCoalition=TargetUnitCoalition or"" -TargetUnitCategory=TargetUnitCategory or"" -TargetUnitType=TargetUnitType or"" -TargetUnitName=TargetUnitName or"" -if lfs and io and os then -self.CSVFile:write( -'"'..self.GameName..'"'..','.. -'"'..self.RunTime..'"'..','.. -''..ScoreTime..''..','.. -'"'..PlayerName..'"'..','.. -'"'..TargetPlayerName..'"'..','.. -'"'..ScoreType..'"'..','.. -'"'..PlayerUnitCoalition..'"'..','.. -'"'..PlayerUnitCategory..'"'..','.. -'"'..PlayerUnitType..'"'..','.. -'"'..PlayerUnitName..'"'..','.. -'"'..TargetUnitCoalition..'"'..','.. -'"'..TargetUnitCategory..'"'..','.. -'"'..TargetUnitType..'"'..','.. -'"'..TargetUnitName..'"'..','.. -''..ScoreTimes..''..','.. -''..ScoreAmount -) -self.CSVFile:write("\n") -end -end -function SCORING:CloseCSV() -if lfs and io and os then -self.CSVFile:close() -end -end -CLEANUP_AIRBASE={ -ClassName="CLEANUP_AIRBASE", -TimeInterval=0.2, -CleanUpList={}, -} -CLEANUP_AIRBASE.__={} -CLEANUP_AIRBASE.__.Airbases={} -function CLEANUP_AIRBASE:New(AirbaseNames) -local self=BASE:Inherit(self,BASE:New()) -self:F({AirbaseNames}) -if type(AirbaseNames)=='table'then -for AirbaseID,AirbaseName in pairs(AirbaseNames)do -self:AddAirbase(AirbaseName) -end -else -local AirbaseName=AirbaseNames -self:AddAirbase(AirbaseName) -end -self:HandleEvent(EVENTS.Birth,self.__.OnEventBirth) -self.__.CleanUpScheduler=SCHEDULER:New(self,self.__.CleanUpSchedule,{},1,self.TimeInterval) -self:HandleEvent(EVENTS.EngineShutdown,self.__.EventAddForCleanUp) -self:HandleEvent(EVENTS.EngineStartup,self.__.EventAddForCleanUp) -self:HandleEvent(EVENTS.Hit,self.__.EventAddForCleanUp) -self:HandleEvent(EVENTS.PilotDead,self.__.OnEventCrash) -self:HandleEvent(EVENTS.Dead,self.__.OnEventCrash) -self:HandleEvent(EVENTS.Crash,self.__.OnEventCrash) -return self -end -function CLEANUP_AIRBASE:AddAirbase(AirbaseName) -self.__.Airbases[AirbaseName]=AIRBASE:FindByName(AirbaseName) -self:F({"Airbase:",AirbaseName,self.__.Airbases[AirbaseName]:GetDesc()}) -return self -end -function CLEANUP_AIRBASE:RemoveAirbase(AirbaseName) -self.__.Airbases[AirbaseName]=nil -return self -end -function CLEANUP_AIRBASE:SetCleanMissiles(CleanMissiles) -if CleanMissiles then -self:HandleEvent(EVENTS.Shot,self.__.OnEventShot) -else -self:UnHandleEvent(EVENTS.Shot) -end -end -function CLEANUP_AIRBASE.__:IsInAirbase(Vec2) -local InAirbase=false -for AirbaseName,Airbase in pairs(self.__.Airbases)do -local Airbase=Airbase -if Airbase:GetZone():IsVec2InZone(Vec2)then -InAirbase=true -break; -end -end -return InAirbase -end -function CLEANUP_AIRBASE.__:DestroyUnit(CleanUpUnit) -self:F({CleanUpUnit}) -if CleanUpUnit then -local CleanUpUnitName=CleanUpUnit:GetName() -local CleanUpGroup=CleanUpUnit:GetGroup() -if CleanUpGroup:IsAlive()then -local CleanUpGroupUnits=CleanUpGroup:GetUnits() -if#CleanUpGroupUnits==1 then -local CleanUpGroupName=CleanUpGroup:GetName() -CleanUpGroup:Destroy() -else -CleanUpUnit:Destroy() -end -self.CleanUpList[CleanUpUnitName]=nil -end -end -end -function CLEANUP_AIRBASE.__:DestroyMissile(MissileObject) -self:F({MissileObject}) -if MissileObject and MissileObject:isExist()then -MissileObject:destroy() -self:T("MissileObject Destroyed") -end -end -function CLEANUP_AIRBASE.__:OnEventBirth(EventData) -self:F({EventData}) -self.CleanUpList[EventData.IniDCSUnitName]={} -self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnit=EventData.IniUnit -self.CleanUpList[EventData.IniDCSUnitName].CleanUpGroup=EventData.IniGroup -self.CleanUpList[EventData.IniDCSUnitName].CleanUpGroupName=EventData.IniDCSGroupName -self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnitName=EventData.IniDCSUnitName -end -function CLEANUP_AIRBASE.__:OnEventCrash(Event) -self:F({Event}) -if Event.IniDCSUnitName and Event.IniCategory==Object.Category.UNIT then -self.CleanUpList[Event.IniDCSUnitName]={} -self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit=Event.IniUnit -self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup=Event.IniGroup -self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName=Event.IniDCSGroupName -self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName=Event.IniDCSUnitName -end -end -function CLEANUP_AIRBASE.__:OnEventShot(Event) -self:F({Event}) -if self:IsInAirbase(Event.IniUnit:GetVec2())then -self:DestroyMissile(Event.Weapon) -end -end -function CLEANUP_AIRBASE.__:OnEventHit(Event) -self:F({Event}) -if Event.IniUnit then -if self:IsInAirbase(Event.IniUnit:GetVec2())then -self:T({"Life: ",Event.IniDCSUnitName,' = ',Event.IniUnit:GetLife(),"/",Event.IniUnit:GetLife0()}) -if Event.IniUnit:GetLife()=SpawnWidth then -SpawnXIndex=0 -SpawnYIndex=SpawnYIndex+1 -end -end -local SpawnRootX=self.SpawnGroups[SpawnGroupID].SpawnTemplate.x -local SpawnRootY=self.SpawnGroups[SpawnGroupID].SpawnTemplate.y -self:_TranslateRotate(SpawnGroupID,SpawnRootX,SpawnRootY,SpawnX,SpawnY,SpawnAngle) -self.SpawnGroups[SpawnGroupID].SpawnTemplate.lateActivation=true -self.SpawnGroups[SpawnGroupID].SpawnTemplate.visible=true -self.SpawnGroups[SpawnGroupID].Visible=true -self:HandleEvent(EVENTS.Birth,self._OnBirth) -self:HandleEvent(EVENTS.Dead,self._OnDeadOrCrash) -self:HandleEvent(EVENTS.Crash,self._OnDeadOrCrash) -if self.Repeat then -self:HandleEvent(EVENTS.Takeoff,self._OnTakeOff) -self:HandleEvent(EVENTS.Land,self._OnLand) -end -if self.RepeatOnEngineShutDown then -self:HandleEvent(EVENTS.EngineShutdown,self._OnEngineShutDown) -end -self.SpawnGroups[SpawnGroupID].Group=_DATABASE:Spawn(self.SpawnGroups[SpawnGroupID].SpawnTemplate) -SpawnX=SpawnXIndex*SpawnDeltaX -SpawnY=SpawnYIndex*SpawnDeltaY -end -return self -end -do -function SPAWN:InitAIOnOff(AIOnOff) -self.AIOnOff=AIOnOff -return self -end -function SPAWN:InitAIOn() -return self:InitAIOnOff(true) -end -function SPAWN:InitAIOff() -return self:InitAIOnOff(false) -end -end -do -function SPAWN:InitDelayOnOff(DelayOnOff) -self.DelayOnOff=DelayOnOff -return self -end -function SPAWN:InitDelayOn() -return self:InitDelayOnOff(true) -end -function SPAWN:InitDelayOff() -return self:InitDelayOnOff(false) -end -end -function SPAWN:Spawn() -self:F({self.SpawnTemplatePrefix,self.SpawnIndex,self.AliveUnits}) -return self:SpawnWithIndex(self.SpawnIndex+1) -end -function SPAWN:ReSpawn(SpawnIndex) -self:F({self.SpawnTemplatePrefix,SpawnIndex}) -if not SpawnIndex then -SpawnIndex=1 -end -local SpawnGroup=self:GetGroupFromIndex(SpawnIndex) -local WayPoints=SpawnGroup and SpawnGroup.WayPoints or nil -if SpawnGroup then -local SpawnDCSGroup=SpawnGroup:GetDCSObject() -if SpawnDCSGroup then -SpawnGroup:Destroy() -end -end -local SpawnGroup=self:SpawnWithIndex(SpawnIndex) -if SpawnGroup and WayPoints then -SpawnGroup:WayPointInitialize(WayPoints) -SpawnGroup:WayPointExecute(1,5) -end -if SpawnGroup.ReSpawnFunction then -SpawnGroup:ReSpawnFunction() -end -SpawnGroup:ResetEvents() -return SpawnGroup -end -function SPAWN:SpawnWithIndex(SpawnIndex) -self:F2({SpawnTemplatePrefix=self.SpawnTemplatePrefix,SpawnIndex=SpawnIndex,AliveUnits=self.AliveUnits,SpawnMaxGroups=self.SpawnMaxGroups}) -if self:_GetSpawnIndex(SpawnIndex)then -if self.SpawnGroups[self.SpawnIndex].Visible then -self.SpawnGroups[self.SpawnIndex].Group:Activate() -else -local SpawnTemplate=self.SpawnGroups[self.SpawnIndex].SpawnTemplate -self:T(SpawnTemplate.name) -if SpawnTemplate then -local PointVec3=POINT_VEC3:New(SpawnTemplate.route.points[1].x,SpawnTemplate.route.points[1].alt,SpawnTemplate.route.points[1].y) -self:T({"Current point of ",self.SpawnTemplatePrefix,PointVec3}) -if self.SpawnRandomizePosition then -local RandomVec2=PointVec3:GetRandomVec2InRadius(self.SpawnRandomizePositionOuterRadius,self.SpawnRandomizePositionInnerRadius) -local CurrentX=SpawnTemplate.units[1].x -local CurrentY=SpawnTemplate.units[1].y -SpawnTemplate.x=RandomVec2.x -SpawnTemplate.y=RandomVec2.y -for UnitID=1,#SpawnTemplate.units do -SpawnTemplate.units[UnitID].x=SpawnTemplate.units[UnitID].x+(RandomVec2.x-CurrentX) -SpawnTemplate.units[UnitID].y=SpawnTemplate.units[UnitID].y+(RandomVec2.y-CurrentY) -self:T('SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) -end -end -if self.SpawnRandomizeUnits then -for UnitID=1,#SpawnTemplate.units do -local RandomVec2=PointVec3:GetRandomVec2InRadius(self.SpawnOuterRadius,self.SpawnInnerRadius) -SpawnTemplate.units[UnitID].x=RandomVec2.x -SpawnTemplate.units[UnitID].y=RandomVec2.y -self:T('SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) -end -end -if SpawnTemplate.CategoryID==Group.Category.HELICOPTER or SpawnTemplate.CategoryID==Group.Category.AIRPLANE then -if SpawnTemplate.route.points[1].type=="TakeOffParking"then -SpawnTemplate.uncontrolled=self.SpawnUnControlled -end -end -end -self:HandleEvent(EVENTS.Birth,self._OnBirth) -self:HandleEvent(EVENTS.Dead,self._OnDeadOrCrash) -self:HandleEvent(EVENTS.Crash,self._OnDeadOrCrash) -if self.Repeat then -self:HandleEvent(EVENTS.Takeoff,self._OnTakeOff) -self:HandleEvent(EVENTS.Land,self._OnLand) -end -if self.RepeatOnEngineShutDown then -self:HandleEvent(EVENTS.EngineShutdown,self._OnEngineShutDown) -end -self.SpawnGroups[self.SpawnIndex].Group=_DATABASE:Spawn(SpawnTemplate) -local SpawnGroup=self.SpawnGroups[self.SpawnIndex].Group -if SpawnGroup then -SpawnGroup:SetAIOnOff(self.AIOnOff) -end -self:T3(SpawnTemplate.name) -if self.SpawnFunctionHook then -self.SpawnHookScheduler=SCHEDULER:New() -self.SpawnHookScheduler:Schedule(nil,self.SpawnFunctionHook,{self.SpawnGroups[self.SpawnIndex].Group,unpack(self.SpawnFunctionArguments)},0.1) -end -end -self.SpawnGroups[self.SpawnIndex].Spawned=true -return self.SpawnGroups[self.SpawnIndex].Group -else -end -return nil -end -function SPAWN:SpawnScheduled(SpawnTime,SpawnTimeVariation) -self:F({SpawnTime,SpawnTimeVariation}) -if SpawnTime~=nil and SpawnTimeVariation~=nil then -local InitialDelay=0 -if self.DelayOnOff==true then -InitialDelay=math.random(SpawnTime-SpawnTime*SpawnTimeVariation,SpawnTime+SpawnTime*SpawnTimeVariation) -end -self.SpawnScheduler=SCHEDULER:New(self,self._Scheduler,{},InitialDelay,SpawnTime,SpawnTimeVariation) -end -return self -end -function SPAWN:SpawnScheduleStart() -self:F({self.SpawnTemplatePrefix}) -self.SpawnScheduler:Start() -return self -end -function SPAWN:SpawnScheduleStop() -self:F({self.SpawnTemplatePrefix}) -self.SpawnScheduler:Stop() -return self -end -function SPAWN:OnSpawnGroup(SpawnCallBackFunction,...) -self:F("OnSpawnGroup") -self.SpawnFunctionHook=SpawnCallBackFunction -self.SpawnFunctionArguments={} -if arg then -self.SpawnFunctionArguments=arg -end -return self -end -function SPAWN:SpawnAtAirbase(SpawnAirbase,Takeoff,TakeoffAltitude) -self:E({self.SpawnTemplatePrefix,SpawnAirbase,Takeoff,TakeoffAltitude}) -local PointVec3=SpawnAirbase:GetPointVec3() -self:T2(PointVec3) -Takeoff=Takeoff or SPAWN.Takeoff.Hot -if self:_GetSpawnIndex(self.SpawnIndex+1)then -local SpawnTemplate=self.SpawnGroups[self.SpawnIndex].SpawnTemplate -if SpawnTemplate then -self:T({"Current point of ",self.SpawnTemplatePrefix,SpawnAirbase}) -local SpawnPoint=SpawnTemplate.route.points[1] -SpawnPoint.linkUnit=nil -SpawnPoint.helipadId=nil -SpawnPoint.airdromeId=nil -local AirbaseID=SpawnAirbase:GetID() -local AirbaseCategory=SpawnAirbase:GetDesc().category -self:F({AirbaseCategory=AirbaseCategory}) -if AirbaseCategory==Airbase.Category.SHIP then -SpawnPoint.linkUnit=AirbaseID -SpawnPoint.helipadId=AirbaseID -elseif AirbaseCategory==Airbase.Category.HELIPAD then -SpawnPoint.linkUnit=AirbaseID -SpawnPoint.helipadId=AirbaseID -elseif AirbaseCategory==Airbase.Category.AIRDROME then -SpawnPoint.airdromeId=AirbaseID -end -SpawnPoint.alt=0 -SpawnPoint.type=GROUPTEMPLATE.Takeoff[Takeoff][1] -SpawnPoint.action=GROUPTEMPLATE.Takeoff[Takeoff][2] -for UnitID=1,#SpawnTemplate.units do -self:T('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) -local UnitTemplate=SpawnTemplate.units[UnitID] -UnitTemplate.parking=nil -UnitTemplate.parking_id=nil -UnitTemplate.alt=0 -local SX=UnitTemplate.x -local SY=UnitTemplate.y -local BX=SpawnPoint.x -local BY=SpawnPoint.y -local TX=PointVec3.x+(SX-BX) -local TY=PointVec3.z+(SY-BY) -UnitTemplate.x=TX -UnitTemplate.y=TY -if Takeoff==GROUP.Takeoff.Air then -UnitTemplate.alt=PointVec3.y+(TakeoffAltitude or 200) -end -self:T('After Translation SpawnTemplate.units['..UnitID..'].x = '..UnitTemplate.x..', SpawnTemplate.units['..UnitID..'].y = '..UnitTemplate.y) -end -SpawnPoint.x=PointVec3.x -SpawnPoint.y=PointVec3.z -if Takeoff==GROUP.Takeoff.Air then -SpawnPoint.alt=PointVec3.y+(TakeoffAltitude or 200) -end -SpawnTemplate.x=PointVec3.x -SpawnTemplate.y=PointVec3.z -local GroupSpawned=self:SpawnWithIndex(self.SpawnIndex) -if Takeoff==GROUP.Takeoff.Air then -for UnitID,UnitSpawned in pairs(GroupSpawned:GetUnits())do -SCHEDULER:New(nil,BASE.CreateEventTakeoff,{GroupSpawned,timer.getTime(),UnitSpawned:GetDCSObject()},1) -end -end -return GroupSpawned -end -end -return nil -end -function SPAWN:SpawnFromVec3(Vec3,SpawnIndex) -self:F({self.SpawnTemplatePrefix,Vec3,SpawnIndex}) -local PointVec3=POINT_VEC3:NewFromVec3(Vec3) -self:T2(PointVec3) -if SpawnIndex then -else -SpawnIndex=self.SpawnIndex+1 -end -if self:_GetSpawnIndex(SpawnIndex)then -local SpawnTemplate=self.SpawnGroups[self.SpawnIndex].SpawnTemplate -if SpawnTemplate then -self:T({"Current point of ",self.SpawnTemplatePrefix,Vec3}) -for UnitID=1,#SpawnTemplate.units do -self:T('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) -local UnitTemplate=SpawnTemplate.units[UnitID] -local SX=UnitTemplate.x -local SY=UnitTemplate.y -local BX=SpawnTemplate.route.points[1].x -local BY=SpawnTemplate.route.points[1].y -local TX=Vec3.x+(SX-BX) -local TY=Vec3.z+(SY-BY) -SpawnTemplate.units[UnitID].x=TX -SpawnTemplate.units[UnitID].y=TY -SpawnTemplate.units[UnitID].alt=Vec3.y -self:T('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) -end -SpawnTemplate.route.points[1].x=Vec3.x -SpawnTemplate.route.points[1].y=Vec3.z -SpawnTemplate.route.points[1].alt=Vec3.y -SpawnTemplate.x=Vec3.x -SpawnTemplate.y=Vec3.z -return self:SpawnWithIndex(self.SpawnIndex) -end -end -return nil -end -function SPAWN:SpawnFromVec2(Vec2,SpawnIndex) -self:F({self.SpawnTemplatePrefix,Vec2,SpawnIndex}) -local PointVec2=POINT_VEC2:NewFromVec2(Vec2) -return self:SpawnFromVec3(PointVec2:GetVec3(),SpawnIndex) -end -function SPAWN:SpawnFromUnit(HostUnit,SpawnIndex) -self:F({self.SpawnTemplatePrefix,HostUnit,SpawnIndex}) -if HostUnit and HostUnit:IsAlive()~=nil then -return self:SpawnFromVec3(HostUnit:GetVec3(),SpawnIndex) -end -return nil -end -function SPAWN:SpawnFromStatic(HostStatic,SpawnIndex) -self:F({self.SpawnTemplatePrefix,HostStatic,SpawnIndex}) -if HostStatic and HostStatic:IsAlive()then -return self:SpawnFromVec3(HostStatic:GetVec3(),SpawnIndex) -end -return nil -end -function SPAWN:SpawnInZone(Zone,RandomizeGroup,SpawnIndex) -self:F({self.SpawnTemplatePrefix,Zone,RandomizeGroup,SpawnIndex}) -if Zone then -if RandomizeGroup then -return self:SpawnFromVec2(Zone:GetRandomVec2(),SpawnIndex) -else -return self:SpawnFromVec2(Zone:GetVec2(),SpawnIndex) -end -end -return nil -end -function SPAWN:InitUnControlled(UnControlled) -self:F2({self.SpawnTemplatePrefix,UnControlled}) -self.SpawnUnControlled=UnControlled -for SpawnGroupID=1,self.SpawnMaxGroups do -self.SpawnGroups[SpawnGroupID].UnControlled=UnControlled -end -return self -end -function SPAWN:GetCoordinate() -local LateGroup=GROUP:FindByName(self.SpawnTemplatePrefix) -if LateGroup then -return LateGroup:GetCoordinate() -end -return nil -end -function SPAWN:SpawnGroupName(SpawnIndex) -self:F({self.SpawnTemplatePrefix,SpawnIndex}) -local SpawnPrefix=self.SpawnTemplatePrefix -if self.SpawnAliasPrefix then -SpawnPrefix=self.SpawnAliasPrefix -end -if SpawnIndex then -local SpawnName=string.format('%s#%03d',SpawnPrefix,SpawnIndex) -self:T(SpawnName) -return SpawnName -else -self:T(SpawnPrefix) -return SpawnPrefix -end -end -function SPAWN:GetFirstAliveGroup() -self:F({self.SpawnTemplatePrefix,self.SpawnAliasPrefix}) -for SpawnIndex=1,self.SpawnCount do -local SpawnGroup=self:GetGroupFromIndex(SpawnIndex) -if SpawnGroup and SpawnGroup:IsAlive()then -return SpawnGroup,SpawnIndex -end -end -return nil,nil -end -function SPAWN:GetNextAliveGroup(SpawnIndexStart) -self:F({self.SpawnTemplatePrefix,self.SpawnAliasPrefix,SpawnIndexStart}) -SpawnIndexStart=SpawnIndexStart+1 -for SpawnIndex=SpawnIndexStart,self.SpawnCount do -local SpawnGroup=self:GetGroupFromIndex(SpawnIndex) -if SpawnGroup and SpawnGroup:IsAlive()then -return SpawnGroup,SpawnIndex -end -end -return nil,nil -end -function SPAWN:GetLastAliveGroup() -self:F({self.SpawnTemplatePrefixself.SpawnAliasPrefix}) -self.SpawnIndex=self:_GetLastIndex() -for SpawnIndex=self.SpawnIndex,1,-1 do -local SpawnGroup=self:GetGroupFromIndex(SpawnIndex) -if SpawnGroup and SpawnGroup:IsAlive()then -self.SpawnIndex=SpawnIndex -return SpawnGroup -end -end -self.SpawnIndex=nil -return nil -end -function SPAWN:GetGroupFromIndex(SpawnIndex) -self:F({self.SpawnTemplatePrefix,self.SpawnAliasPrefix,SpawnIndex}) -if not SpawnIndex then -SpawnIndex=1 -end -if self.SpawnGroups and self.SpawnGroups[SpawnIndex]then -local SpawnGroup=self.SpawnGroups[SpawnIndex].Group -return SpawnGroup -else -return nil -end -end -function SPAWN:_GetPrefixFromGroup(SpawnGroup) -self:F3({self.SpawnTemplatePrefix,self.SpawnAliasPrefix,SpawnGroup}) -local GroupName=SpawnGroup:GetName() -if GroupName then -local SpawnPrefix=string.match(GroupName,".*#") -if SpawnPrefix then -SpawnPrefix=SpawnPrefix:sub(1,-2) -end -return SpawnPrefix -end -return nil -end -function SPAWN:GetSpawnIndexFromGroup(SpawnGroup) -self:F({self.SpawnTemplatePrefix,self.SpawnAliasPrefix,SpawnGroup}) -local IndexString=string.match(SpawnGroup:GetName(),"#(%d*)$"):sub(2) -local Index=tonumber(IndexString) -self:T3(IndexString,Index) -return Index -end -function SPAWN:_GetLastIndex() -self:F({self.SpawnTemplatePrefix,self.SpawnAliasPrefix}) -return self.SpawnMaxGroups -end -function SPAWN:_InitializeSpawnGroups(SpawnIndex) -self:F3({self.SpawnTemplatePrefix,self.SpawnAliasPrefix,SpawnIndex}) -if not self.SpawnGroups[SpawnIndex]then -self.SpawnGroups[SpawnIndex]={} -self.SpawnGroups[SpawnIndex].Visible=false -self.SpawnGroups[SpawnIndex].Spawned=false -self.SpawnGroups[SpawnIndex].UnControlled=false -self.SpawnGroups[SpawnIndex].SpawnTime=0 -self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix=self.SpawnTemplatePrefix -self.SpawnGroups[SpawnIndex].SpawnTemplate=self:_Prepare(self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix,SpawnIndex) -end -self:_RandomizeTemplate(SpawnIndex) -self:_RandomizeRoute(SpawnIndex) -return self.SpawnGroups[SpawnIndex] -end -function SPAWN:_GetGroupCategoryID(SpawnPrefix) -local TemplateGroup=Group.getByName(SpawnPrefix) -if TemplateGroup then -return TemplateGroup:getCategory() -else -return nil -end -end -function SPAWN:_GetGroupCoalitionID(SpawnPrefix) -local TemplateGroup=Group.getByName(SpawnPrefix) -if TemplateGroup then -return TemplateGroup:getCoalition() -else -return nil -end -end -function SPAWN:_GetGroupCountryID(SpawnPrefix) -self:F({self.SpawnTemplatePrefix,self.SpawnAliasPrefix,SpawnPrefix}) -local TemplateGroup=Group.getByName(SpawnPrefix) -if TemplateGroup then -local TemplateUnits=TemplateGroup:getUnits() -return TemplateUnits[1]:getCountry() -else -return nil -end -end -function SPAWN:_GetTemplate(SpawnTemplatePrefix) -self:F({self.SpawnTemplatePrefix,self.SpawnAliasPrefix,SpawnTemplatePrefix}) -local SpawnTemplate=nil -SpawnTemplate=routines.utils.deepCopy(_DATABASE.Templates.Groups[SpawnTemplatePrefix].Template) -if SpawnTemplate==nil then -error('No Template returned for SpawnTemplatePrefix = '..SpawnTemplatePrefix) -end -self:T3({SpawnTemplate}) -return SpawnTemplate -end -function SPAWN:_Prepare(SpawnTemplatePrefix,SpawnIndex) -self:F({self.SpawnTemplatePrefix,self.SpawnAliasPrefix}) -local SpawnTemplate=self:_GetTemplate(SpawnTemplatePrefix) -SpawnTemplate.name=self:SpawnGroupName(SpawnIndex) -SpawnTemplate.groupId=nil -SpawnTemplate.lateActivation=false -if SpawnTemplate.CategoryID==Group.Category.GROUND then -self:T3("For ground units, visible needs to be false...") -SpawnTemplate.visible=false -end -if self.SpawnGrouping then -local UnitAmount=#SpawnTemplate.units -self:F({UnitAmount=UnitAmount,SpawnGrouping=self.SpawnGrouping}) -if UnitAmount>self.SpawnGrouping then -for UnitID=self.SpawnGrouping+1,UnitAmount do -SpawnTemplate.units[UnitID]=nil -end -else -if UnitAmount0 then -local MoveProbability=(self.MoveMaximum*100)/self.AliveUnits -self:T('Move Probability = '..MoveProbability) -for MovementUnitName,MovementGroupName in pairs(self.MoveUnits)do -local MovementGroup=Group.getByName(MovementGroupName) -if MovementGroup and MovementGroup:isExist()then -local MoveOrStop=math.random(1,100) -self:T('MoveOrStop = '..MoveOrStop) -if MoveOrStop<=MoveProbability then -self:T('Group continues moving = '..MovementGroupName) -trigger.action.groupContinueMoving(MovementGroup) -else -self:T('Group stops moving = '..MovementGroupName) -trigger.action.groupStopMoving(MovementGroup) -end -else -self.MoveUnits[MovementUnitName]=nil -end -end -end -return true -end -SEAD={ -ClassName="SEAD", -TargetSkill={ -Average={Evade=50,DelayOff={10,25},DelayOn={10,30}}, -Good={Evade=30,DelayOff={8,20},DelayOn={20,40}}, -High={Evade=15,DelayOff={5,17},DelayOn={30,50}}, -Excellent={Evade=10,DelayOff={3,10},DelayOn={30,60}} -}, -SEADGroupPrefixes={} -} -function SEAD:New(SEADGroupPrefixes) -local self=BASE:Inherit(self,BASE:New()) -self:F(SEADGroupPrefixes) -if type(SEADGroupPrefixes)=='table'then -for SEADGroupPrefixID,SEADGroupPrefix in pairs(SEADGroupPrefixes)do -self.SEADGroupPrefixes[SEADGroupPrefix]=SEADGroupPrefix -end -else -self.SEADGroupNames[SEADGroupPrefixes]=SEADGroupPrefixes -end -self:HandleEvent(EVENTS.Shot) -return self -end -function SEAD:OnEventShot(EventData) -self:F({EventData}) -local SEADUnit=EventData.IniDCSUnit -local SEADUnitName=EventData.IniDCSUnitName -local SEADWeapon=EventData.Weapon -local SEADWeaponName=EventData.WeaponName -self:T("Missile Launched = "..SEADWeaponName) -if SEADWeaponName=="KH-58"or SEADWeaponName=="KH-25MPU"or SEADWeaponName=="AGM-88"or SEADWeaponName=="KH-31A"or SEADWeaponName=="KH-31P"then -local _evade=math.random(1,100) -local _targetMim=EventData.Weapon:getTarget() -local _targetMimname=Unit.getName(_targetMim) -local _targetMimgroup=Unit.getGroup(Weapon.getTarget(SEADWeapon)) -local _targetMimgroupName=_targetMimgroup:getName() -local _targetMimcont=_targetMimgroup:getController() -local _targetskill=_DATABASE.Templates.Units[_targetMimname].Template.skill -self:T(self.SEADGroupPrefixes) -self:T(_targetMimgroupName) -local SEADGroupFound=false -for SEADGroupPrefixID,SEADGroupPrefix in pairs(self.SEADGroupPrefixes)do -if string.find(_targetMimgroupName,SEADGroupPrefix,1,true)then -SEADGroupFound=true -self:T('Group Found') -break -end -end -if SEADGroupFound==true then -if _targetskill=="Random"then -local Skills={"Average","Good","High","Excellent"} -_targetskill=Skills[math.random(1,4)] -end -self:T(_targetskill) -if self.TargetSkill[_targetskill]then -if(_evade>self.TargetSkill[_targetskill].Evade)then -self:T(string.format("Evading, target skill "..string.format(_targetskill))) -local _targetMim=Weapon.getTarget(SEADWeapon) -local _targetMimname=Unit.getName(_targetMim) -local _targetMimgroup=Unit.getGroup(Weapon.getTarget(SEADWeapon)) -local _targetMimcont=_targetMimgroup:getController() -routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -local SuppressedGroups1={} -local function SuppressionEnd1(id) -id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) -SuppressedGroups1[id.groupName]=nil -end -local id={ -groupName=_targetMimgroup, -ctrl=_targetMimcont -} -local delay1=math.random(self.TargetSkill[_targetskill].DelayOff[1],self.TargetSkill[_targetskill].DelayOff[2]) -if SuppressedGroups1[id.groupName]==nil then -SuppressedGroups1[id.groupName]={ -SuppressionEndTime1=timer.getTime()+delay1, -SuppressionEndN1=SuppressionEndCounter1 -} -Controller.setOption(_targetMimcont,AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) -timer.scheduleFunction(SuppressionEnd1,id,SuppressedGroups1[id.groupName].SuppressionEndTime1) -end -local SuppressedGroups={} -local function SuppressionEnd(id) -id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) -SuppressedGroups[id.groupName]=nil -end -local id={ -groupName=_targetMimgroup, -ctrl=_targetMimcont -} -local delay=math.random(self.TargetSkill[_targetskill].DelayOn[1],self.TargetSkill[_targetskill].DelayOn[2]) -if SuppressedGroups[id.groupName]==nil then -SuppressedGroups[id.groupName]={ -SuppressionEndTime=timer.getTime()+delay, -SuppressionEndN=SuppressionEndCounter -} -timer.scheduleFunction(SuppressionEnd,id,SuppressedGroups[id.groupName].SuppressionEndTime) -end -end -end -end -end -end -ESCORT={ -ClassName="ESCORT", -EscortName=nil, -EscortClient=nil, -EscortGroup=nil, -EscortMode=1, -MODE={ -FOLLOW=1, -MISSION=2, -}, -Targets={}, -FollowScheduler=nil, -ReportTargets=true, -OptionROE=AI.Option.Air.val.ROE.OPEN_FIRE, -OptionReactionOnThreat=AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION, -SmokeDirectionVector=false, -TaskPoints={} -} -function ESCORT:New(EscortClient,EscortGroup,EscortName,EscortBriefing) -local self=BASE:Inherit(self,BASE:New()) -self:F({EscortClient,EscortGroup,EscortName}) -self.EscortClient=EscortClient -self.EscortGroup=EscortGroup -self.EscortName=EscortName -self.EscortBriefing=EscortBriefing -self.EscortSetGroup=SET_GROUP:New() -self.EscortSetGroup:AddObject(self.EscortGroup) -self.EscortSetGroup:Flush() -self.Detection=DETECTION_UNITS:New(self.EscortSetGroup,15000) -self.EscortGroup.Detection=self.Detection -if not self.EscortClient._EscortGroups then -self.EscortClient._EscortGroups={} -end -if not self.EscortClient._EscortGroups[EscortGroup:GetName()]then -self.EscortClient._EscortGroups[EscortGroup:GetName()]={} -self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortGroup=self.EscortGroup -self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortName=self.EscortName -self.EscortClient._EscortGroups[EscortGroup:GetName()].Detection=self.EscortGroup.Detection -end -self.EscortMenu=MENU_CLIENT:New(self.EscortClient,self.EscortName) -self.EscortGroup:WayPointInitialize(1) -self.EscortGroup:OptionROTVertical() -self.EscortGroup:OptionROEOpenFire() -if not EscortBriefing then -EscortGroup:MessageToClient(EscortGroup:GetCategoryName().." '"..EscortName.."' ("..EscortGroup:GetCallsign()..") reporting! ".. -"We're escorting your flight. ".. -"Use the Radio Menu and F10 and use the options under + "..EscortName.."\n", -60,EscortClient -) -else -EscortGroup:MessageToClient(EscortGroup:GetCategoryName().." '"..EscortName.."' ("..EscortGroup:GetCallsign()..") "..EscortBriefing, -60,EscortClient -) -end -self.FollowDistance=100 -self.CT1=0 -self.GT1=0 -self.FollowScheduler,self.FollowSchedule=SCHEDULER:New(self,self._FollowScheduler,{},1,.5,.01) -self.FollowScheduler:Stop(self.FollowSchedule) -self.EscortMode=ESCORT.MODE.MISSION -return self -end -function ESCORT:SetDetection(Detection) -self.Detection=Detection -self.EscortGroup.Detection=self.Detection -self.EscortClient._EscortGroups[self.EscortGroup:GetName()].Detection=self.EscortGroup.Detection -Detection:__Start(1) -end -function ESCORT:TestSmokeDirectionVector(SmokeDirection) -self.SmokeDirectionVector=(SmokeDirection==true)and true or false -end -function ESCORT:Menus() -self:F() -self:MenuFollowAt(100) -self:MenuFollowAt(200) -self:MenuFollowAt(300) -self:MenuFollowAt(400) -self:MenuScanForTargets(100,60) -self:MenuHoldAtEscortPosition(30) -self:MenuHoldAtLeaderPosition(30) -self:MenuFlare() -self:MenuSmoke() -self:MenuReportTargets(60) -self:MenuAssistedAttack() -self:MenuROE() -self:MenuEvasion() -self:MenuResumeMission() -return self -end -function ESCORT:MenuFollowAt(Distance) -self:F(Distance) -if self.EscortGroup:IsAir()then -if not self.EscortMenuReportNavigation then -self.EscortMenuReportNavigation=MENU_CLIENT:New(self.EscortClient,"Navigation",self.EscortMenu) -end -if not self.EscortMenuJoinUpAndFollow then -self.EscortMenuJoinUpAndFollow={} -end -self.EscortMenuJoinUpAndFollow[#self.EscortMenuJoinUpAndFollow+1]=MENU_CLIENT_COMMAND:New(self.EscortClient,"Join-Up and Follow at "..Distance,self.EscortMenuReportNavigation,ESCORT._JoinUpAndFollow,self,Distance) -self.EscortMode=ESCORT.MODE.FOLLOW -end -return self -end -function ESCORT:MenuHoldAtEscortPosition(Height,Seconds,MenuTextFormat) -self:F({Height,Seconds,MenuTextFormat}) -if self.EscortGroup:IsAir()then -if not self.EscortMenuHold then -self.EscortMenuHold=MENU_CLIENT:New(self.EscortClient,"Hold position",self.EscortMenu) -end -if not Height then -Height=30 -end -if not Seconds then -Seconds=0 -end -local MenuText="" -if not MenuTextFormat then -if Seconds==0 then -MenuText=string.format("Hold at %d meter",Height) -else -MenuText=string.format("Hold at %d meter for %d seconds",Height,Seconds) -end -else -if Seconds==0 then -MenuText=string.format(MenuTextFormat,Height) -else -MenuText=string.format(MenuTextFormat,Height,Seconds) -end -end -if not self.EscortMenuHoldPosition then -self.EscortMenuHoldPosition={} -end -self.EscortMenuHoldPosition[#self.EscortMenuHoldPosition+1]=MENU_CLIENT_COMMAND -:New( -self.EscortClient, -MenuText, -self.EscortMenuHold, -ESCORT._HoldPosition, -self, -self.EscortGroup, -Height, -Seconds -) -end -return self -end -function ESCORT:MenuHoldAtLeaderPosition(Height,Seconds,MenuTextFormat) -self:F({Height,Seconds,MenuTextFormat}) -if self.EscortGroup:IsAir()then -if not self.EscortMenuHold then -self.EscortMenuHold=MENU_CLIENT:New(self.EscortClient,"Hold position",self.EscortMenu) -end -if not Height then -Height=30 -end -if not Seconds then -Seconds=0 -end -local MenuText="" -if not MenuTextFormat then -if Seconds==0 then -MenuText=string.format("Rejoin and hold at %d meter",Height) -else -MenuText=string.format("Rejoin and hold at %d meter for %d seconds",Height,Seconds) -end -else -if Seconds==0 then -MenuText=string.format(MenuTextFormat,Height) -else -MenuText=string.format(MenuTextFormat,Height,Seconds) -end -end -if not self.EscortMenuHoldAtLeaderPosition then -self.EscortMenuHoldAtLeaderPosition={} -end -self.EscortMenuHoldAtLeaderPosition[#self.EscortMenuHoldAtLeaderPosition+1]=MENU_CLIENT_COMMAND -:New( -self.EscortClient, -MenuText, -self.EscortMenuHold, -ESCORT._HoldPosition, -{ParamSelf=self, -ParamOrbitGroup=self.EscortClient, -ParamHeight=Height, -ParamSeconds=Seconds -} -) -end -return self -end -function ESCORT:MenuScanForTargets(Height,Seconds,MenuTextFormat) -self:F({Height,Seconds,MenuTextFormat}) -if self.EscortGroup:IsAir()then -if not self.EscortMenuScan then -self.EscortMenuScan=MENU_CLIENT:New(self.EscortClient,"Scan for targets",self.EscortMenu) -end -if not Height then -Height=100 -end -if not Seconds then -Seconds=30 -end -local MenuText="" -if not MenuTextFormat then -if Seconds==0 then -MenuText=string.format("At %d meter",Height) -else -MenuText=string.format("At %d meter for %d seconds",Height,Seconds) -end -else -if Seconds==0 then -MenuText=string.format(MenuTextFormat,Height) -else -MenuText=string.format(MenuTextFormat,Height,Seconds) -end -end -if not self.EscortMenuScanForTargets then -self.EscortMenuScanForTargets={} -end -self.EscortMenuScanForTargets[#self.EscortMenuScanForTargets+1]=MENU_CLIENT_COMMAND -:New( -self.EscortClient, -MenuText, -self.EscortMenuScan, -ESCORT._ScanTargets, -self, -30 -) -end -return self -end -function ESCORT:MenuFlare(MenuTextFormat) -self:F() -if not self.EscortMenuReportNavigation then -self.EscortMenuReportNavigation=MENU_CLIENT:New(self.EscortClient,"Navigation",self.EscortMenu) -end -local MenuText="" -if not MenuTextFormat then -MenuText="Flare" -else -MenuText=MenuTextFormat -end -if not self.EscortMenuFlare then -self.EscortMenuFlare=MENU_CLIENT:New(self.EscortClient,MenuText,self.EscortMenuReportNavigation,ESCORT._Flare,self) -self.EscortMenuFlareGreen=MENU_CLIENT_COMMAND:New(self.EscortClient,"Release green flare",self.EscortMenuFlare,ESCORT._Flare,self,FLARECOLOR.Green,"Released a green flare!") -self.EscortMenuFlareRed=MENU_CLIENT_COMMAND:New(self.EscortClient,"Release red flare",self.EscortMenuFlare,ESCORT._Flare,self,FLARECOLOR.Red,"Released a red flare!") -self.EscortMenuFlareWhite=MENU_CLIENT_COMMAND:New(self.EscortClient,"Release white flare",self.EscortMenuFlare,ESCORT._Flare,self,FLARECOLOR.White,"Released a white flare!") -self.EscortMenuFlareYellow=MENU_CLIENT_COMMAND:New(self.EscortClient,"Release yellow flare",self.EscortMenuFlare,ESCORT._Flare,self,FLARECOLOR.Yellow,"Released a yellow flare!") -end -return self -end -function ESCORT:MenuSmoke(MenuTextFormat) -self:F() -if not self.EscortGroup:IsAir()then -if not self.EscortMenuReportNavigation then -self.EscortMenuReportNavigation=MENU_CLIENT:New(self.EscortClient,"Navigation",self.EscortMenu) -end -local MenuText="" -if not MenuTextFormat then -MenuText="Smoke" -else -MenuText=MenuTextFormat -end -if not self.EscortMenuSmoke then -self.EscortMenuSmoke=MENU_CLIENT:New(self.EscortClient,"Smoke",self.EscortMenuReportNavigation,ESCORT._Smoke,self) -self.EscortMenuSmokeGreen=MENU_CLIENT_COMMAND:New(self.EscortClient,"Release green smoke",self.EscortMenuSmoke,ESCORT._Smoke,self,SMOKECOLOR.Green,"Releasing green smoke!") -self.EscortMenuSmokeRed=MENU_CLIENT_COMMAND:New(self.EscortClient,"Release red smoke",self.EscortMenuSmoke,ESCORT._Smoke,self,SMOKECOLOR.Red,"Releasing red smoke!") -self.EscortMenuSmokeWhite=MENU_CLIENT_COMMAND:New(self.EscortClient,"Release white smoke",self.EscortMenuSmoke,ESCORT._Smoke,self,SMOKECOLOR.White,"Releasing white smoke!") -self.EscortMenuSmokeOrange=MENU_CLIENT_COMMAND:New(self.EscortClient,"Release orange smoke",self.EscortMenuSmoke,ESCORT._Smoke,self,SMOKECOLOR.Orange,"Releasing orange smoke!") -self.EscortMenuSmokeBlue=MENU_CLIENT_COMMAND:New(self.EscortClient,"Release blue smoke",self.EscortMenuSmoke,ESCORT._Smoke,self,SMOKECOLOR.Blue,"Releasing blue smoke!") -end -end -return self -end -function ESCORT:MenuReportTargets(Seconds) -self:F({Seconds}) -if not self.EscortMenuReportNearbyTargets then -self.EscortMenuReportNearbyTargets=MENU_CLIENT:New(self.EscortClient,"Report targets",self.EscortMenu) -end -if not Seconds then -Seconds=30 -end -self.EscortMenuReportNearbyTargetsNow=MENU_CLIENT_COMMAND:New(self.EscortClient,"Report targets now!",self.EscortMenuReportNearbyTargets,ESCORT._ReportNearbyTargetsNow,self) -self.EscortMenuReportNearbyTargetsOn=MENU_CLIENT_COMMAND:New(self.EscortClient,"Report targets on",self.EscortMenuReportNearbyTargets,ESCORT._SwitchReportNearbyTargets,self,true) -self.EscortMenuReportNearbyTargetsOff=MENU_CLIENT_COMMAND:New(self.EscortClient,"Report targets off",self.EscortMenuReportNearbyTargets,ESCORT._SwitchReportNearbyTargets,self,false) -self.EscortMenuAttackNearbyTargets=MENU_CLIENT:New(self.EscortClient,"Attack targets",self.EscortMenu) -self.ReportTargetsScheduler=SCHEDULER:New(self,self._ReportTargetsScheduler,{},1,Seconds) -return self -end -function ESCORT:MenuAssistedAttack() -self:F() -self.EscortMenuTargetAssistance=MENU_CLIENT:New(self.EscortClient,"Request assistance from",self.EscortMenu) -return self -end -function ESCORT:MenuROE(MenuTextFormat) -self:F(MenuTextFormat) -if not self.EscortMenuROE then -self.EscortMenuROE=MENU_CLIENT:New(self.EscortClient,"ROE",self.EscortMenu) -if self.EscortGroup:OptionROEHoldFirePossible()then -self.EscortMenuROEHoldFire=MENU_CLIENT_COMMAND:New(self.EscortClient,"Hold Fire",self.EscortMenuROE,ESCORT._ROE,self,self.EscortGroup:OptionROEHoldFire(),"Holding weapons!") -end -if self.EscortGroup:OptionROEReturnFirePossible()then -self.EscortMenuROEReturnFire=MENU_CLIENT_COMMAND:New(self.EscortClient,"Return Fire",self.EscortMenuROE,ESCORT._ROE,self,self.EscortGroup:OptionROEReturnFire(),"Returning fire!") -end -if self.EscortGroup:OptionROEOpenFirePossible()then -self.EscortMenuROEOpenFire=MENU_CLIENT_COMMAND:New(self.EscortClient,"Open Fire",self.EscortMenuROE,ESCORT._ROE,self,self.EscortGroup:OptionROEOpenFire(),"Opening fire on designated targets!!") -end -if self.EscortGroup:OptionROEWeaponFreePossible()then -self.EscortMenuROEWeaponFree=MENU_CLIENT_COMMAND:New(self.EscortClient,"Weapon Free",self.EscortMenuROE,ESCORT._ROE,self,self.EscortGroup:OptionROEWeaponFree(),"Opening fire on targets of opportunity!") -end -end -return self -end -function ESCORT:MenuEvasion(MenuTextFormat) -self:F(MenuTextFormat) -if self.EscortGroup:IsAir()then -if not self.EscortMenuEvasion then -self.EscortMenuEvasion=MENU_CLIENT:New(self.EscortClient,"Evasion",self.EscortMenu) -if self.EscortGroup:OptionROTNoReactionPossible()then -self.EscortMenuEvasionNoReaction=MENU_CLIENT_COMMAND:New(self.EscortClient,"Fight until death",self.EscortMenuEvasion,ESCORT._ROT,self,self.EscortGroup:OptionROTNoReaction(),"Fighting until death!") -end -if self.EscortGroup:OptionROTPassiveDefensePossible()then -self.EscortMenuEvasionPassiveDefense=MENU_CLIENT_COMMAND:New(self.EscortClient,"Use flares, chaff and jammers",self.EscortMenuEvasion,ESCORT._ROT,self,self.EscortGroup:OptionROTPassiveDefense(),"Defending using jammers, chaff and flares!") -end -if self.EscortGroup:OptionROTEvadeFirePossible()then -self.EscortMenuEvasionEvadeFire=MENU_CLIENT_COMMAND:New(self.EscortClient,"Evade enemy fire",self.EscortMenuEvasion,ESCORT._ROT,self,self.EscortGroup:OptionROTEvadeFire(),"Evading on enemy fire!") -end -if self.EscortGroup:OptionROTVerticalPossible()then -self.EscortMenuOptionEvasionVertical=MENU_CLIENT_COMMAND:New(self.EscortClient,"Go below radar and evade fire",self.EscortMenuEvasion,ESCORT._ROT,self,self.EscortGroup:OptionROTVertical(),"Evading on enemy fire with vertical manoeuvres!") -end -end -end -return self -end -function ESCORT:MenuResumeMission() -self:F() -if not self.EscortMenuResumeMission then -self.EscortMenuResumeMission=MENU_CLIENT:New(self.EscortClient,"Resume mission from",self.EscortMenu) -end -return self -end -function ESCORT:_HoldPosition(OrbitGroup,OrbitHeight,OrbitSeconds) -local EscortGroup=self.EscortGroup -local EscortClient=self.EscortClient -local OrbitUnit=OrbitGroup:GetUnit(1) -self.FollowScheduler:Stop(self.FollowSchedule) -local PointFrom={} -local GroupVec3=EscortGroup:GetUnit(1):GetVec3() -PointFrom={} -PointFrom.x=GroupVec3.x -PointFrom.y=GroupVec3.z -PointFrom.speed=250 -PointFrom.type=AI.Task.WaypointType.TURNING_POINT -PointFrom.alt=GroupVec3.y -PointFrom.alt_type=AI.Task.AltitudeType.BARO -local OrbitPoint=OrbitUnit:GetVec2() -local PointTo={} -PointTo.x=OrbitPoint.x -PointTo.y=OrbitPoint.y -PointTo.speed=250 -PointTo.type=AI.Task.WaypointType.TURNING_POINT -PointTo.alt=OrbitHeight -PointTo.alt_type=AI.Task.AltitudeType.BARO -PointTo.task=EscortGroup:TaskOrbitCircleAtVec2(OrbitPoint,OrbitHeight,0) -local Points={PointFrom,PointTo} -EscortGroup:OptionROEHoldFire() -EscortGroup:OptionROTPassiveDefense() -EscortGroup:SetTask(EscortGroup:TaskRoute(Points)) -EscortGroup:MessageToClient("Orbiting at location.",10,EscortClient) -end -function ESCORT:_JoinUpAndFollow(Distance) -local EscortGroup=self.EscortGroup -local EscortClient=self.EscortClient -self.Distance=Distance -self:JoinUpAndFollow(EscortGroup,EscortClient,self.Distance) -end -function ESCORT:JoinUpAndFollow(EscortGroup,EscortClient,Distance) -self:F({EscortGroup,EscortClient,Distance}) -self.FollowScheduler:Stop(self.FollowSchedule) -EscortGroup:OptionROEHoldFire() -EscortGroup:OptionROTPassiveDefense() -self.EscortMode=ESCORT.MODE.FOLLOW -self.CT1=0 -self.GT1=0 -self.FollowScheduler:Start(self.FollowSchedule) -EscortGroup:MessageToClient("Rejoining and Following at "..Distance.."!",30,EscortClient) -end -function ESCORT:_Flare(Color,Message) -local EscortGroup=self.EscortGroup -local EscortClient=self.EscortClient -EscortGroup:GetUnit(1):Flare(Color) -EscortGroup:MessageToClient(Message,10,EscortClient) -end -function ESCORT:_Smoke(Color,Message) -local EscortGroup=self.EscortGroup -local EscortClient=self.EscortClient -EscortGroup:GetUnit(1):Smoke(Color) -EscortGroup:MessageToClient(Message,10,EscortClient) -end -function ESCORT:_ReportNearbyTargetsNow() -local EscortGroup=self.EscortGroup -local EscortClient=self.EscortClient -self:_ReportTargetsScheduler() -end -function ESCORT:_SwitchReportNearbyTargets(ReportTargets) -local EscortGroup=self.EscortGroup -local EscortClient=self.EscortClient -self.ReportTargets=ReportTargets -if self.ReportTargets then -if not self.ReportTargetsScheduler then -self.ReportTargetsScheduler:Schedule(self,self._ReportTargetsScheduler,{},1,30) -end -else -routines.removeFunction(self.ReportTargetsScheduler) -self.ReportTargetsScheduler=nil -end -end -function ESCORT:_ScanTargets(ScanDuration) -local EscortGroup=self.EscortGroup -local EscortClient=self.EscortClient -self.FollowScheduler:Stop(self.FollowSchedule) -if EscortGroup:IsHelicopter()then -EscortGroup:PushTask( -EscortGroup:TaskControlled( -EscortGroup:TaskOrbitCircle(200,20), -EscortGroup:TaskCondition(nil,nil,nil,nil,ScanDuration,nil) -),1) -elseif EscortGroup:IsAirPlane()then -EscortGroup:PushTask( -EscortGroup:TaskControlled( -EscortGroup:TaskOrbitCircle(1000,500), -EscortGroup:TaskCondition(nil,nil,nil,nil,ScanDuration,nil) -),1) -end -EscortGroup:MessageToClient("Scanning targets for "..ScanDuration.." seconds.",ScanDuration,EscortClient) -if self.EscortMode==ESCORT.MODE.FOLLOW then -self.FollowScheduler:Start(self.FollowSchedule) -end -end -function _Resume(EscortGroup) -env.info('_Resume') -local Escort=EscortGroup:GetState(EscortGroup,"Escort") -env.info("EscortMode = "..Escort.EscortMode) -if Escort.EscortMode==ESCORT.MODE.FOLLOW then -Escort:JoinUpAndFollow(EscortGroup,Escort.EscortClient,Escort.Distance) -end -end -function ESCORT:_AttackTarget(DetectedItemID) -local EscortGroup=self.EscortGroup -self:E(EscortGroup) -local EscortClient=self.EscortClient -self.FollowScheduler:Stop(self.FollowSchedule) -if EscortGroup:IsAir()then -EscortGroup:OptionROEOpenFire() -EscortGroup:OptionROTPassiveDefense() -EscortGroup:SetState(EscortGroup,"Escort",self) -local DetectedSet=self.Detection:GetDetectedSet(DetectedItemID) -local Tasks={} -DetectedSet:ForEachUnit( -function(DetectedUnit,Tasks) -if DetectedUnit:IsAlive()then -Tasks[#Tasks+1]=EscortGroup:TaskAttackUnit(DetectedUnit) -end -end,Tasks -) -Tasks[#Tasks+1]=EscortGroup:TaskFunction("_Resume",{"''"}) -EscortGroup:SetTask( -EscortGroup:TaskCombo( -Tasks -),1 -) -else -local DetectedSet=self.Detection:GetDetectedSet(DetectedItemID) -local Tasks={} -DetectedSet:ForEachUnit( -function(DetectedUnit,Tasks) -if DetectedUnit:IsAlive()then -Tasks[#Tasks+1]=EscortGroup:TaskFireAtPoint(DetectedUnit:GetVec2(),50) -end -end,Tasks -) -EscortGroup:SetTask( -EscortGroup:TaskCombo( -Tasks -),1 -) -end -EscortGroup:MessageToClient("Engaging Designated Unit!",10,EscortClient) -end -function ESCORT:_AssistTarget(EscortGroupAttack,DetectedItemID) -local EscortGroup=self.EscortGroup -local EscortClient=self.EscortClient -self.FollowScheduler:Stop(self.FollowSchedule) -if EscortGroupAttack:IsAir()then -EscortGroupAttack:OptionROEOpenFire() -EscortGroupAttack:OptionROTVertical() -local DetectedSet=self.Detection:GetDetectedSet(DetectedItemID) -local Tasks={} -DetectedSet:ForEachUnit( -function(DetectedUnit,Tasks) -if DetectedUnit:IsAlive()then -Tasks[#Tasks+1]=EscortGroupAttack:TaskAttackUnit(DetectedUnit) -end -end,Tasks -) -Tasks[#Tasks+1]=EscortGroupAttack:TaskOrbitCircle(500,350) -EscortGroupAttack:SetTask( -EscortGroupAttack:TaskCombo( -Tasks -),1 -) -else -local DetectedSet=self.Detection:GetDetectedSet(DetectedItemID) -local Tasks={} -DetectedSet:ForEachUnit( -function(DetectedUnit,Tasks) -if DetectedUnit:IsAlive()then -Tasks[#Tasks+1]=EscortGroupAttack:TaskFireAtPoint(DetectedUnit:GetVec2(),50) -end -end,Tasks -) -EscortGroupAttack:SetTask( -EscortGroupAttack:TaskCombo( -Tasks -),1 -) -end -EscortGroupAttack:MessageToClient("Assisting with the destroying the enemy unit!",10,EscortClient) -end -function ESCORT:_ROE(EscortROEFunction,EscortROEMessage) -local EscortGroup=self.EscortGroup -local EscortClient=self.EscortClient -pcall(function()EscortROEFunction()end) -EscortGroup:MessageToClient(EscortROEMessage,10,EscortClient) -end -function ESCORT:_ROT(EscortROTFunction,EscortROTMessage) -local EscortGroup=self.EscortGroup -local EscortClient=self.EscortClient -pcall(function()EscortROTFunction()end) -EscortGroup:MessageToClient(EscortROTMessage,10,EscortClient) -end -function ESCORT:_ResumeMission(WayPoint) -local EscortGroup=self.EscortGroup -local EscortClient=self.EscortClient -self.FollowScheduler:Stop(self.FollowSchedule) -local WayPoints=EscortGroup:GetTaskRoute() -self:T(WayPoint,WayPoints) -for WayPointIgnore=1,WayPoint do -table.remove(WayPoints,1) -end -SCHEDULER:New(EscortGroup,EscortGroup.SetTask,{EscortGroup:TaskRoute(WayPoints)},1) -EscortGroup:MessageToClient("Resuming mission from waypoint "..WayPoint..".",10,EscortClient) -end -function ESCORT:RegisterRoute() -self:F() -local EscortGroup=self.EscortGroup -local TaskPoints=EscortGroup:GetTaskRoute() -self:T(TaskPoints) -return TaskPoints -end -function ESCORT:_FollowScheduler() -self:F({self.FollowDistance}) -self:T({self.EscortClient.UnitName,self.EscortGroup.GroupName}) -if self.EscortGroup:IsAlive()and self.EscortClient:IsAlive()then -local ClientUnit=self.EscortClient:GetClientGroupUnit() -local GroupUnit=self.EscortGroup:GetUnit(1) -local FollowDistance=self.FollowDistance -self:T({ClientUnit.UnitName,GroupUnit.UnitName}) -if self.CT1==0 and self.GT1==0 then -self.CV1=ClientUnit:GetVec3() -self:T({"self.CV1",self.CV1}) -self.CT1=timer.getTime() -self.GV1=GroupUnit:GetVec3() -self.GT1=timer.getTime() -else -local CT1=self.CT1 -local CT2=timer.getTime() -local CV1=self.CV1 -local CV2=ClientUnit:GetVec3() -self.CT1=CT2 -self.CV1=CV2 -local CD=((CV2.x-CV1.x)^2+(CV2.y-CV1.y)^2+(CV2.z-CV1.z)^2)^0.5 -local CT=CT2-CT1 -local CS=(3600/CT)*(CD/1000) -self:T2({"Client:",CS,CD,CT,CV2,CV1,CT2,CT1}) -local GT1=self.GT1 -local GT2=timer.getTime() -local GV1=self.GV1 -local GV2=GroupUnit:GetVec3() -self.GT1=GT2 -self.GV1=GV2 -local GD=((GV2.x-GV1.x)^2+(GV2.y-GV1.y)^2+(GV2.z-GV1.z)^2)^0.5 -local GT=GT2-GT1 -local GS=(3600/GT)*(GD/1000) -self:T2({"Group:",GS,GD,GT,GV2,GV1,GT2,GT1}) -local GV={x=GV2.x-CV2.x,y=GV2.y-CV2.y,z=GV2.z-CV2.z} -local GH2={x=GV2.x,y=CV2.y,z=GV2.z} -local alpha=math.atan2(GV.z,GV.x) -local CVI={x=CV2.x+FollowDistance*math.cos(alpha), -y=GH2.y, -z=CV2.z+FollowDistance*math.sin(alpha), -} -local DV={x=CV2.x-CVI.x,y=CV2.y-CVI.y,z=CV2.z-CVI.z} -local DVu={x=DV.x/FollowDistance,y=DV.y/FollowDistance,z=DV.z/FollowDistance} -local GDV={x=DVu.x*CS*8+CVI.x,y=CVI.y,z=DVu.z*CS*8+CVI.z} -if self.SmokeDirectionVector==true then -trigger.action.smoke(GDV,trigger.smokeColor.Red) -end -self:T2({"CV2:",CV2}) -self:T2({"CVI:",CVI}) -self:T2({"GDV:",GDV}) -local CatchUpDistance=((GDV.x-GV2.x)^2+(GDV.y-GV2.y)^2+(GDV.z-GV2.z)^2)^0.5 -local Time=10 -local CatchUpSpeed=(CatchUpDistance-(CS*8.4))/Time -local Speed=CS+CatchUpSpeed -if Speed<0 then -Speed=0 -end -self:T({"Client Speed, Escort Speed, Speed, FollowDistance, Time:",CS,GS,Speed,FollowDistance,Time}) -self.EscortGroup:RouteToVec3(GDV,Speed/3.6) -end -return true -end -return false -end -function ESCORT:_ReportTargetsScheduler() -self:F(self.EscortGroup:GetName()) -if self.EscortGroup:IsAlive()and self.EscortClient:IsAlive()then -if true then -local EscortGroupName=self.EscortGroup:GetName() -self.EscortMenuAttackNearbyTargets:RemoveSubMenus() -if self.EscortMenuTargetAssistance then -self.EscortMenuTargetAssistance:RemoveSubMenus() -end -local DetectedItems=self.Detection:GetDetectedItems() -self:E(DetectedItems) -local DetectedTargets=false -local DetectedMsgs={} -for ClientEscortGroupName,EscortGroupData in pairs(self.EscortClient._EscortGroups)do -local ClientEscortTargets=EscortGroupData.Detection -for DetectedItemID,DetectedItem in pairs(DetectedItems)do -self:E({DetectedItemID,DetectedItem}) -local DetectedItemReportSummary=self.Detection:DetectedItemReportSummary(DetectedItemID,EscortGroupData.EscortGroup,_DATABASE:GetPlayerSettings(self.EscortClient:GetPlayerName())) -if ClientEscortGroupName==EscortGroupName then -local DetectedMsg=DetectedItemReportSummary:Text("\n") -DetectedMsgs[#DetectedMsgs+1]=DetectedMsg -self:T(DetectedMsg) -MENU_CLIENT_COMMAND:New(self.EscortClient, -DetectedMsg, -self.EscortMenuAttackNearbyTargets, -ESCORT._AttackTarget, -self, -DetectedItemID -) -else -if self.EscortMenuTargetAssistance then -local DetectedMsg=DetectedItemReportSummary:Text("\n") -self:T(DetectedMsg) -local MenuTargetAssistance=MENU_CLIENT:New(self.EscortClient,EscortGroupData.EscortName,self.EscortMenuTargetAssistance) -MENU_CLIENT_COMMAND:New(self.EscortClient, -DetectedMsg, -MenuTargetAssistance, -ESCORT._AssistTarget, -self, -EscortGroupData.EscortGroup, -DetectedItemID -) -end -end -DetectedTargets=true -end -end -self:E(DetectedMsgs) -if DetectedTargets then -self.EscortGroup:MessageToClient("Reporting detected targets:\n"..table.concat(DetectedMsgs,"\n"),20,self.EscortClient) -else -self.EscortGroup:MessageToClient("No targets detected.",10,self.EscortClient) -end -return true -else -end -end -return false -end -MISSILETRAINER={ -ClassName="MISSILETRAINER", -TrackingMissiles={}, -} -function MISSILETRAINER._Alive(Client,self) -if self.Briefing then -Client:Message(self.Briefing,15,"Trainer") -end -if self.MenusOnOff==true then -Client:Message("Use the 'Radio Menu' -> 'Other (F10)' -> 'Missile Trainer' menu options to change the Missile Trainer settings (for all players).",15,"Trainer") -Client.MainMenu=MENU_CLIENT:New(Client,"Missile Trainer",nil) -Client.MenuMessages=MENU_CLIENT:New(Client,"Messages",Client.MainMenu) -Client.MenuOn=MENU_CLIENT_COMMAND:New(Client,"Messages On",Client.MenuMessages,self._MenuMessages,{MenuSelf=self,MessagesOnOff=true}) -Client.MenuOff=MENU_CLIENT_COMMAND:New(Client,"Messages Off",Client.MenuMessages,self._MenuMessages,{MenuSelf=self,MessagesOnOff=false}) -Client.MenuTracking=MENU_CLIENT:New(Client,"Tracking",Client.MainMenu) -Client.MenuTrackingToAll=MENU_CLIENT_COMMAND:New(Client,"To All",Client.MenuTracking,self._MenuMessages,{MenuSelf=self,TrackingToAll=true}) -Client.MenuTrackingToTarget=MENU_CLIENT_COMMAND:New(Client,"To Target",Client.MenuTracking,self._MenuMessages,{MenuSelf=self,TrackingToAll=false}) -Client.MenuTrackOn=MENU_CLIENT_COMMAND:New(Client,"Tracking On",Client.MenuTracking,self._MenuMessages,{MenuSelf=self,TrackingOnOff=true}) -Client.MenuTrackOff=MENU_CLIENT_COMMAND:New(Client,"Tracking Off",Client.MenuTracking,self._MenuMessages,{MenuSelf=self,TrackingOnOff=false}) -Client.MenuTrackIncrease=MENU_CLIENT_COMMAND:New(Client,"Frequency Increase",Client.MenuTracking,self._MenuMessages,{MenuSelf=self,TrackingFrequency=-1}) -Client.MenuTrackDecrease=MENU_CLIENT_COMMAND:New(Client,"Frequency Decrease",Client.MenuTracking,self._MenuMessages,{MenuSelf=self,TrackingFrequency=1}) -Client.MenuAlerts=MENU_CLIENT:New(Client,"Alerts",Client.MainMenu) -Client.MenuAlertsToAll=MENU_CLIENT_COMMAND:New(Client,"To All",Client.MenuAlerts,self._MenuMessages,{MenuSelf=self,AlertsToAll=true}) -Client.MenuAlertsToTarget=MENU_CLIENT_COMMAND:New(Client,"To Target",Client.MenuAlerts,self._MenuMessages,{MenuSelf=self,AlertsToAll=false}) -Client.MenuHitsOn=MENU_CLIENT_COMMAND:New(Client,"Hits On",Client.MenuAlerts,self._MenuMessages,{MenuSelf=self,AlertsHitsOnOff=true}) -Client.MenuHitsOff=MENU_CLIENT_COMMAND:New(Client,"Hits Off",Client.MenuAlerts,self._MenuMessages,{MenuSelf=self,AlertsHitsOnOff=false}) -Client.MenuLaunchesOn=MENU_CLIENT_COMMAND:New(Client,"Launches On",Client.MenuAlerts,self._MenuMessages,{MenuSelf=self,AlertsLaunchesOnOff=true}) -Client.MenuLaunchesOff=MENU_CLIENT_COMMAND:New(Client,"Launches Off",Client.MenuAlerts,self._MenuMessages,{MenuSelf=self,AlertsLaunchesOnOff=false}) -Client.MenuDetails=MENU_CLIENT:New(Client,"Details",Client.MainMenu) -Client.MenuDetailsDistanceOn=MENU_CLIENT_COMMAND:New(Client,"Range On",Client.MenuDetails,self._MenuMessages,{MenuSelf=self,DetailsRangeOnOff=true}) -Client.MenuDetailsDistanceOff=MENU_CLIENT_COMMAND:New(Client,"Range Off",Client.MenuDetails,self._MenuMessages,{MenuSelf=self,DetailsRangeOnOff=false}) -Client.MenuDetailsBearingOn=MENU_CLIENT_COMMAND:New(Client,"Bearing On",Client.MenuDetails,self._MenuMessages,{MenuSelf=self,DetailsBearingOnOff=true}) -Client.MenuDetailsBearingOff=MENU_CLIENT_COMMAND:New(Client,"Bearing Off",Client.MenuDetails,self._MenuMessages,{MenuSelf=self,DetailsBearingOnOff=false}) -Client.MenuDistance=MENU_CLIENT:New(Client,"Set distance to plane",Client.MainMenu) -Client.MenuDistance50=MENU_CLIENT_COMMAND:New(Client,"50 meter",Client.MenuDistance,self._MenuMessages,{MenuSelf=self,Distance=50/1000}) -Client.MenuDistance100=MENU_CLIENT_COMMAND:New(Client,"100 meter",Client.MenuDistance,self._MenuMessages,{MenuSelf=self,Distance=100/1000}) -Client.MenuDistance150=MENU_CLIENT_COMMAND:New(Client,"150 meter",Client.MenuDistance,self._MenuMessages,{MenuSelf=self,Distance=150/1000}) -Client.MenuDistance200=MENU_CLIENT_COMMAND:New(Client,"200 meter",Client.MenuDistance,self._MenuMessages,{MenuSelf=self,Distance=200/1000}) -else -if Client.MainMenu then -Client.MainMenu:Remove() -end -end -local ClientID=Client:GetID() -self:T(ClientID) -if not self.TrackingMissiles[ClientID]then -self.TrackingMissiles[ClientID]={} -end -self.TrackingMissiles[ClientID].Client=Client -if not self.TrackingMissiles[ClientID].MissileData then -self.TrackingMissiles[ClientID].MissileData={} -end -end -function MISSILETRAINER:New(Distance,Briefing) -local self=BASE:Inherit(self,BASE:New()) -self:F(Distance) -if Briefing then -self.Briefing=Briefing -end -self.Schedulers={} -self.SchedulerID=0 -self.MessageInterval=2 -self.MessageLastTime=timer.getTime() -self.Distance=Distance/1000 -self:HandleEvent(EVENTS.Shot) -self.DBClients=SET_CLIENT:New():FilterStart() -self.DBClients:ForEachClient( -function(Client) -self:E("ForEach:"..Client.UnitName) -Client:Alive(self._Alive,self) -end -) -self.MessagesOnOff=true -self.TrackingToAll=false -self.TrackingOnOff=true -self.TrackingFrequency=3 -self.AlertsToAll=true -self.AlertsHitsOnOff=true -self.AlertsLaunchesOnOff=true -self.DetailsRangeOnOff=true -self.DetailsBearingOnOff=true -self.MenusOnOff=true -self.TrackingMissiles={} -self.TrackingScheduler=SCHEDULER:New(self,self._TrackMissiles,{},0.5,0.05,0) -return self -end -function MISSILETRAINER:InitMessagesOnOff(MessagesOnOff) -self:F(MessagesOnOff) -self.MessagesOnOff=MessagesOnOff -if self.MessagesOnOff==true then -MESSAGE:New("Messages ON",15,"Menu"):ToAll() -else -MESSAGE:New("Messages OFF",15,"Menu"):ToAll() -end -return self -end -function MISSILETRAINER:InitTrackingToAll(TrackingToAll) -self:F(TrackingToAll) -self.TrackingToAll=TrackingToAll -if self.TrackingToAll==true then -MESSAGE:New("Missile tracking to all players ON",15,"Menu"):ToAll() -else -MESSAGE:New("Missile tracking to all players OFF",15,"Menu"):ToAll() -end -return self -end -function MISSILETRAINER:InitTrackingOnOff(TrackingOnOff) -self:F(TrackingOnOff) -self.TrackingOnOff=TrackingOnOff -if self.TrackingOnOff==true then -MESSAGE:New("Missile tracking ON",15,"Menu"):ToAll() -else -MESSAGE:New("Missile tracking OFF",15,"Menu"):ToAll() -end -return self -end -function MISSILETRAINER:InitTrackingFrequency(TrackingFrequency) -self:F(TrackingFrequency) -self.TrackingFrequency=self.TrackingFrequency+TrackingFrequency -if self.TrackingFrequency<0.5 then -self.TrackingFrequency=0.5 -end -if self.TrackingFrequency then -MESSAGE:New("Missile tracking frequency is "..self.TrackingFrequency.." seconds.",15,"Menu"):ToAll() -end -return self -end -function MISSILETRAINER:InitAlertsToAll(AlertsToAll) -self:F(AlertsToAll) -self.AlertsToAll=AlertsToAll -if self.AlertsToAll==true then -MESSAGE:New("Alerts to all players ON",15,"Menu"):ToAll() -else -MESSAGE:New("Alerts to all players OFF",15,"Menu"):ToAll() -end -return self -end -function MISSILETRAINER:InitAlertsHitsOnOff(AlertsHitsOnOff) -self:F(AlertsHitsOnOff) -self.AlertsHitsOnOff=AlertsHitsOnOff -if self.AlertsHitsOnOff==true then -MESSAGE:New("Alerts Hits ON",15,"Menu"):ToAll() -else -MESSAGE:New("Alerts Hits OFF",15,"Menu"):ToAll() -end -return self -end -function MISSILETRAINER:InitAlertsLaunchesOnOff(AlertsLaunchesOnOff) -self:F(AlertsLaunchesOnOff) -self.AlertsLaunchesOnOff=AlertsLaunchesOnOff -if self.AlertsLaunchesOnOff==true then -MESSAGE:New("Alerts Launches ON",15,"Menu"):ToAll() -else -MESSAGE:New("Alerts Launches OFF",15,"Menu"):ToAll() -end -return self -end -function MISSILETRAINER:InitRangeOnOff(DetailsRangeOnOff) -self:F(DetailsRangeOnOff) -self.DetailsRangeOnOff=DetailsRangeOnOff -if self.DetailsRangeOnOff==true then -MESSAGE:New("Range display ON",15,"Menu"):ToAll() -else -MESSAGE:New("Range display OFF",15,"Menu"):ToAll() -end -return self -end -function MISSILETRAINER:InitBearingOnOff(DetailsBearingOnOff) -self:F(DetailsBearingOnOff) -self.DetailsBearingOnOff=DetailsBearingOnOff -if self.DetailsBearingOnOff==true then -MESSAGE:New("Bearing display OFF",15,"Menu"):ToAll() -else -MESSAGE:New("Bearing display OFF",15,"Menu"):ToAll() -end -return self -end -function MISSILETRAINER:InitMenusOnOff(MenusOnOff) -self:F(MenusOnOff) -self.MenusOnOff=MenusOnOff -if self.MenusOnOff==true then -MESSAGE:New("Menus are ENABLED (only when a player rejoins a slot)",15,"Menu"):ToAll() -else -MESSAGE:New("Menus are DISABLED",15,"Menu"):ToAll() -end -return self -end -function MISSILETRAINER._MenuMessages(MenuParameters) -local self=MenuParameters.MenuSelf -if MenuParameters.MessagesOnOff~=nil then -self:InitMessagesOnOff(MenuParameters.MessagesOnOff) -end -if MenuParameters.TrackingToAll~=nil then -self:InitTrackingToAll(MenuParameters.TrackingToAll) -end -if MenuParameters.TrackingOnOff~=nil then -self:InitTrackingOnOff(MenuParameters.TrackingOnOff) -end -if MenuParameters.TrackingFrequency~=nil then -self:InitTrackingFrequency(MenuParameters.TrackingFrequency) -end -if MenuParameters.AlertsToAll~=nil then -self:InitAlertsToAll(MenuParameters.AlertsToAll) -end -if MenuParameters.AlertsHitsOnOff~=nil then -self:InitAlertsHitsOnOff(MenuParameters.AlertsHitsOnOff) -end -if MenuParameters.AlertsLaunchesOnOff~=nil then -self:InitAlertsLaunchesOnOff(MenuParameters.AlertsLaunchesOnOff) -end -if MenuParameters.DetailsRangeOnOff~=nil then -self:InitRangeOnOff(MenuParameters.DetailsRangeOnOff) -end -if MenuParameters.DetailsBearingOnOff~=nil then -self:InitBearingOnOff(MenuParameters.DetailsBearingOnOff) -end -if MenuParameters.Distance~=nil then -self.Distance=MenuParameters.Distance -MESSAGE:New("Hit detection distance set to "..(self.Distance*1000).." meters",15,"Menu"):ToAll() -end -end -function MISSILETRAINER:OnEventShot(EVentData) -self:F({EVentData}) -local TrainerSourceDCSUnit=EVentData.IniDCSUnit -local TrainerSourceDCSUnitName=EVentData.IniDCSUnitName -local TrainerWeapon=EVentData.Weapon -local TrainerWeaponName=EVentData.WeaponName -self:T("Missile Launched = "..TrainerWeaponName) -local TrainerTargetDCSUnit=TrainerWeapon:getTarget() -if TrainerTargetDCSUnit then -local TrainerTargetDCSUnitName=Unit.getName(TrainerTargetDCSUnit) -local TrainerTargetSkill=_DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill -self:T(TrainerTargetDCSUnitName) -local Client=self.DBClients:FindClient(TrainerTargetDCSUnitName) -if Client then -local TrainerSourceUnit=UNIT:Find(TrainerSourceDCSUnit) -local TrainerTargetUnit=UNIT:Find(TrainerTargetDCSUnit) -if self.MessagesOnOff==true and self.AlertsLaunchesOnOff==true then -local Message=MESSAGE:New( -string.format("%s launched a %s", -TrainerSourceUnit:GetTypeName(), -TrainerWeaponName -)..self:_AddRange(Client,TrainerWeapon)..self:_AddBearing(Client,TrainerWeapon),5,"Launch Alert") -if self.AlertsToAll then -Message:ToAll() -else -Message:ToClient(Client) -end -end -local ClientID=Client:GetID() -self:T(ClientID) -local MissileData={} -MissileData.TrainerSourceUnit=TrainerSourceUnit -MissileData.TrainerWeapon=TrainerWeapon -MissileData.TrainerTargetUnit=TrainerTargetUnit -MissileData.TrainerWeaponTypeName=TrainerWeapon:getTypeName() -MissileData.TrainerWeaponLaunched=true -table.insert(self.TrackingMissiles[ClientID].MissileData,MissileData) -end -else -if(TrainerWeapon:getTypeName()=="9M311")then -SCHEDULER:New(TrainerWeapon,TrainerWeapon.destroy,{},1) -else -end -end -end -function MISSILETRAINER:_AddRange(Client,TrainerWeapon) -local RangeText="" -if self.DetailsRangeOnOff then -local PositionMissile=TrainerWeapon:getPoint() -local TargetVec3=Client:GetVec3() -local Range=((PositionMissile.x-TargetVec3.x)^2+ -(PositionMissile.y-TargetVec3.y)^2+ -(PositionMissile.z-TargetVec3.z)^2 -)^0.5/1000 -RangeText=string.format(", at %4.2fkm",Range) -end -return RangeText -end -function MISSILETRAINER:_AddBearing(Client,TrainerWeapon) -local BearingText="" -if self.DetailsBearingOnOff then -local PositionMissile=TrainerWeapon:getPoint() -local TargetVec3=Client:GetVec3() -self:T2({TargetVec3,PositionMissile}) -local DirectionVector={x=PositionMissile.x-TargetVec3.x,y=PositionMissile.y-TargetVec3.y,z=PositionMissile.z-TargetVec3.z} -local DirectionRadians=math.atan2(DirectionVector.z,DirectionVector.x) -if DirectionRadians<0 then -DirectionRadians=DirectionRadians+2*math.pi -end -local DirectionDegrees=DirectionRadians*180/math.pi -BearingText=string.format(", %d degrees",DirectionDegrees) -end -return BearingText -end -function MISSILETRAINER:_TrackMissiles() -self:F2() -local ShowMessages=false -if self.MessagesOnOff and self.MessageLastTime+self.TrackingFrequency<=timer.getTime()then -self.MessageLastTime=timer.getTime() -ShowMessages=true -end -for ClientDataID,ClientData in pairs(self.TrackingMissiles)do -local Client=ClientData.Client -if Client and Client:IsAlive()then -for MissileDataID,MissileData in pairs(ClientData.MissileData)do -self:T3(MissileDataID) -local TrainerSourceUnit=MissileData.TrainerSourceUnit -local TrainerWeapon=MissileData.TrainerWeapon -local TrainerTargetUnit=MissileData.TrainerTargetUnit -local TrainerWeaponTypeName=MissileData.TrainerWeaponTypeName -local TrainerWeaponLaunched=MissileData.TrainerWeaponLaunched -if Client and Client:IsAlive()and TrainerSourceUnit and TrainerSourceUnit:IsAlive()and TrainerWeapon and TrainerWeapon:isExist()and TrainerTargetUnit and TrainerTargetUnit:IsAlive()then -local PositionMissile=TrainerWeapon:getPosition().p -local TargetVec3=Client:GetVec3() -local Distance=((PositionMissile.x-TargetVec3.x)^2+ -(PositionMissile.y-TargetVec3.y)^2+ -(PositionMissile.z-TargetVec3.z)^2 -)^0.5/1000 -if Distance<=self.Distance then -TrainerWeapon:destroy() -if self.MessagesOnOff==true and self.AlertsHitsOnOff==true then -self:T("killed") -local Message=MESSAGE:New( -string.format("%s launched by %s killed %s", -TrainerWeapon:getTypeName(), -TrainerSourceUnit:GetTypeName(), -TrainerTargetUnit:GetPlayerName() -),15,"Hit Alert") -if self.AlertsToAll==true then -Message:ToAll() -else -Message:ToClient(Client) -end -MissileData=nil -table.remove(ClientData.MissileData,MissileDataID) -self:T(ClientData.MissileData) -end -end -else -if not(TrainerWeapon and TrainerWeapon:isExist())then -if self.MessagesOnOff==true and self.AlertsLaunchesOnOff==true then -local Message=MESSAGE:New( -string.format("%s launched by %s self destructed!", -TrainerWeaponTypeName, -TrainerSourceUnit:GetTypeName() -),5,"Tracking") -if self.AlertsToAll==true then -Message:ToAll() -else -Message:ToClient(Client) -end -end -MissileData=nil -table.remove(ClientData.MissileData,MissileDataID) -self:T(ClientData.MissileData) -end -end -end -else -self.TrackingMissiles[ClientDataID]=nil -end -end -if ShowMessages==true and self.MessagesOnOff==true and self.TrackingOnOff==true then -for ClientDataID,ClientData in pairs(self.TrackingMissiles)do -local Client=ClientData.Client -ClientData.MessageToClient="" -ClientData.MessageToAll="" -for TrackingDataID,TrackingData in pairs(self.TrackingMissiles)do -for MissileDataID,MissileData in pairs(TrackingData.MissileData)do -local TrainerSourceUnit=MissileData.TrainerSourceUnit -local TrainerWeapon=MissileData.TrainerWeapon -local TrainerTargetUnit=MissileData.TrainerTargetUnit -local TrainerWeaponTypeName=MissileData.TrainerWeaponTypeName -local TrainerWeaponLaunched=MissileData.TrainerWeaponLaunched -if Client and Client:IsAlive()and TrainerSourceUnit and TrainerSourceUnit:IsAlive()and TrainerWeapon and TrainerWeapon:isExist()and TrainerTargetUnit and TrainerTargetUnit:IsAlive()then -if ShowMessages==true then -local TrackingTo -TrackingTo=string.format(" -> %s", -TrainerWeaponTypeName -) -if ClientDataID==TrackingDataID then -if ClientData.MessageToClient==""then -ClientData.MessageToClient="Missiles to You:\n" -end -ClientData.MessageToClient=ClientData.MessageToClient..TrackingTo..self:_AddRange(ClientData.Client,TrainerWeapon)..self:_AddBearing(ClientData.Client,TrainerWeapon).."\n" -else -if self.TrackingToAll==true then -if ClientData.MessageToAll==""then -ClientData.MessageToAll="Missiles to other Players:\n" -end -ClientData.MessageToAll=ClientData.MessageToAll..TrackingTo..self:_AddRange(ClientData.Client,TrainerWeapon)..self:_AddBearing(ClientData.Client,TrainerWeapon).." ( "..TrainerTargetUnit:GetPlayerName().." )\n" -end -end -end -end -end -end -if ClientData.MessageToClient~=""or ClientData.MessageToAll~=""then -local Message=MESSAGE:New(ClientData.MessageToClient..ClientData.MessageToAll,1,"Tracking"):ToClient(Client) -end -end -end -return true -end -AIRBASEPOLICE_BASE={ -ClassName="AIRBASEPOLICE_BASE", -SetClient=nil, -Airbases=nil, -AirbaseNames=nil, -} -function AIRBASEPOLICE_BASE:New(SetClient,Airbases) -local self=BASE:Inherit(self,BASE:New()) -self:E({self.ClassName,SetClient,Airbases}) -self.SetClient=SetClient -self.Airbases=Airbases -for AirbaseID,Airbase in pairs(self.Airbases)do -Airbase.ZoneBoundary=ZONE_POLYGON_BASE:New("Boundary",Airbase.PointsBoundary):SmokeZone(SMOKECOLOR.White):Flush() -for PointsRunwayID,PointsRunway in pairs(Airbase.PointsRunways)do -Airbase.ZoneRunways[PointsRunwayID]=ZONE_POLYGON_BASE:New("Runway "..PointsRunwayID,PointsRunway):SmokeZone(SMOKECOLOR.Red):Flush() -end -end -self.SetClient:ForEachClient( -function(Client) -Client:SetState(self,"Speeding",false) -Client:SetState(self,"Warnings",0) -Client:SetState(self,"Taxi",false) -end -) -self.AirbaseMonitor=SCHEDULER:New(self,self._AirbaseMonitor,{},0,2,0.05) -return self -end -function AIRBASEPOLICE_BASE:Monitor(AirbaseNames) -if AirbaseNames then -if type(AirbaseNames)=="table"then -self.AirbaseNames=AirbaseNames -else -self.AirbaseNames={AirbaseNames} -end -end -end -function AIRBASEPOLICE_BASE:_AirbaseMonitor() -for AirbaseID,Airbase in pairs(self.Airbases)do -if not self.AirbaseNames or self.AirbaseNames[AirbaseID]then -self:E(AirbaseID) -self.SetClient:ForEachClientInZone(Airbase.ZoneBoundary, -function(Client) -self:E(Client.UnitName) -if Client:IsAlive()then -local NotInRunwayZone=true -for ZoneRunwayID,ZoneRunway in pairs(Airbase.ZoneRunways)do -NotInRunwayZone=(Client:IsNotInZone(ZoneRunway)==true)and NotInRunwayZone or false -end -if NotInRunwayZone then -local Taxi=self:GetState(self,"Taxi") -self:E(Taxi) -if Taxi==false then -Client:Message("Welcome at "..AirbaseID..". The maximum taxiing speed is "..Airbase.MaximumSpeed" km/h.",20,"ATC") -self:SetState(self,"Taxi",true) -end -local VelocityVec3=Client:GetVelocity() -local Velocity=(VelocityVec3.x^2+VelocityVec3.y^2+VelocityVec3.z^2)^0.5 -local Velocity=Velocity*3.6 -local IsAboveRunway=Client:IsAboveRunway() -local IsOnGround=Client:InAir()==false -self:T(IsAboveRunway,IsOnGround) -if IsAboveRunway and IsOnGround then -if Velocity>Airbase.MaximumSpeed then -local IsSpeeding=Client:GetState(self,"Speeding") -if IsSpeeding==true then -local SpeedingWarnings=Client:GetState(self,"Warnings") -self:T(SpeedingWarnings) -if SpeedingWarnings<=3 then -Client:Message("You are speeding on the taxiway! Slow down or you will be removed from this airbase! Your current velocity is "..string.format("%2.0f km/h",Velocity),5,"Warning "..SpeedingWarnings.." / 3") -Client:SetState(self,"Warnings",SpeedingWarnings+1) -else -MESSAGE:New("Player "..Client:GetPlayerName().." is being damaged at the airbase, due to a speeding violation ...",10,"Airbase Police"):ToAll() -local function DestroyUntilHeavilyDamaged(Client) -local ClientCoord=Client:GetCoordinate() -ClientCoord:Explosion(100) -local Damage=Client:GetLife() -local InitialLife=Client:GetLife0() -MESSAGE:New("Player "..Client:GetPlayerName().." Damage ... "..Damage,5,"Airbase Police"):ToAll() -if(Damage/InitialLife)*100<80 then -Client:ScheduleStop(DestroyUntilHeavilyDamaged) -end -end -Client:ScheduleOnce(1,DestroyUntilHeavilyDamaged,Client) -trigger.action.setUserFlag("AIRCRAFT_"..Client:GetID(),100) -Client:SetState(self,"Speeding",false) -Client:SetState(self,"Warnings",0) -end -else -Client:Message("You are speeding on the taxiway, slow down now! Your current velocity is "..string.format("%2.0f km/h",Velocity),5,"Attention! ") -Client:SetState(self,"Speeding",true) -Client:SetState(self,"Warnings",1) -end -else -Client:SetState(self,"Speeding",false) -Client:SetState(self,"Warnings",0) -end -end -else -Client:SetState(self,"Speeding",false) -Client:SetState(self,"Warnings",0) -local Taxi=self:GetState(self,"Taxi") -if Taxi==true then -Client:Message("You have progressed to the runway ... Await take-off clearance ...",20,"ATC") -self:SetState(self,"Taxi",false) -end -end -end -end -) -end -end -return true -end -AIRBASEPOLICE_CAUCASUS={ -ClassName="AIRBASEPOLICE_CAUCASUS", -Airbases={ -AnapaVityazevo={ -PointsBoundary={ -[1]={["y"]=242234.85714287,["x"]=-6616.5714285726,}, -[2]={["y"]=241060.57142858,["x"]=-5585.142857144,}, -[3]={["y"]=243806.2857143,["x"]=-3962.2857142868,}, -[4]={["y"]=245240.57142858,["x"]=-4816.5714285726,}, -[5]={["y"]=244783.42857144,["x"]=-5630.8571428583,}, -[6]={["y"]=243800.57142858,["x"]=-5065.142857144,}, -[7]={["y"]=242232.00000001,["x"]=-6622.2857142868,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=242140.57142858,["x"]=-6478.8571428583,}, -[2]={["y"]=242188.57142858,["x"]=-6522.0000000011,}, -[3]={["y"]=244124.2857143,["x"]=-4344.0000000011,}, -[4]={["y"]=244068.2857143,["x"]=-4296.5714285726,}, -[5]={["y"]=242140.57142858,["x"]=-6480.0000000011,} -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -Batumi={ -PointsBoundary={ -[1]={["y"]=617567.14285714,["x"]=-355313.14285715,}, -[2]={["y"]=616181.42857142,["x"]=-354800.28571429,}, -[3]={["y"]=616007.14285714,["x"]=-355128.85714286,}, -[4]={["y"]=618230,["x"]=-356914.57142858,}, -[5]={["y"]=618727.14285714,["x"]=-356166,}, -[6]={["y"]=617572.85714285,["x"]=-355308.85714286,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=616442.28571429,["x"]=-355090.28571429,}, -[2]={["y"]=618450.57142857,["x"]=-356522,}, -[3]={["y"]=618407.71428571,["x"]=-356584.85714286,}, -[4]={["y"]=618361.99999999,["x"]=-356554.85714286,}, -[5]={["y"]=618324.85714285,["x"]=-356599.14285715,}, -[6]={["y"]=618250.57142856,["x"]=-356543.42857143,}, -[7]={["y"]=618257.7142857,["x"]=-356496.28571429,}, -[8]={["y"]=618237.7142857,["x"]=-356459.14285715,}, -[9]={["y"]=616555.71428571,["x"]=-355258.85714286,}, -[10]={["y"]=616486.28571428,["x"]=-355280.57142858,}, -[11]={["y"]=616410.57142856,["x"]=-355227.71428572,}, -[12]={["y"]=616441.99999999,["x"]=-355179.14285715,}, -[13]={["y"]=616401.99999999,["x"]=-355147.71428572,}, -[14]={["y"]=616441.42857142,["x"]=-355092.57142858,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -Beslan={ -PointsBoundary={ -[1]={["y"]=842082.57142857,["x"]=-148445.14285715,}, -[2]={["y"]=845237.71428572,["x"]=-148639.71428572,}, -[3]={["y"]=845232,["x"]=-148765.42857143,}, -[4]={["y"]=844220.57142857,["x"]=-149168.28571429,}, -[5]={["y"]=843274.85714286,["x"]=-149125.42857143,}, -[6]={["y"]=842077.71428572,["x"]=-148554,}, -[7]={["y"]=842083.42857143,["x"]=-148445.42857143,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=842104.57142857,["x"]=-148460.57142857,}, -[2]={["y"]=845225.71428572,["x"]=-148656,}, -[3]={["y"]=845220.57142858,["x"]=-148750,}, -[4]={["y"]=842098.85714286,["x"]=-148556.28571429,}, -[5]={["y"]=842104,["x"]=-148460.28571429,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -Gelendzhik={ -PointsBoundary={ -[1]={["y"]=297856.00000001,["x"]=-51151.428571429,}, -[2]={["y"]=299044.57142858,["x"]=-49720.000000001,}, -[3]={["y"]=298861.71428572,["x"]=-49580.000000001,}, -[4]={["y"]=298198.85714286,["x"]=-49842.857142858,}, -[5]={["y"]=297990.28571429,["x"]=-50151.428571429,}, -[6]={["y"]=297696.00000001,["x"]=-51054.285714286,}, -[7]={["y"]=297850.28571429,["x"]=-51160.000000001,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=297834.00000001,["x"]=-51107.428571429,}, -[2]={["y"]=297786.57142858,["x"]=-51068.857142858,}, -[3]={["y"]=298946.57142858,["x"]=-49686.000000001,}, -[4]={["y"]=298993.14285715,["x"]=-49725.714285715,}, -[5]={["y"]=297835.14285715,["x"]=-51107.714285715,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -Gudauta={ -PointsBoundary={ -[1]={["y"]=517246.57142857,["x"]=-197850.28571429,}, -[2]={["y"]=516749.42857142,["x"]=-198070.28571429,}, -[3]={["y"]=515755.14285714,["x"]=-197598.85714286,}, -[4]={["y"]=515369.42857142,["x"]=-196538.85714286,}, -[5]={["y"]=515623.71428571,["x"]=-195618.85714286,}, -[6]={["y"]=515946.57142857,["x"]=-195510.28571429,}, -[7]={["y"]=517243.71428571,["x"]=-197858.85714286,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=517096.57142857,["x"]=-197804.57142857,}, -[2]={["y"]=515880.85714285,["x"]=-195590.28571429,}, -[3]={["y"]=515812.28571428,["x"]=-195628.85714286,}, -[4]={["y"]=517036.57142857,["x"]=-197834.57142857,}, -[5]={["y"]=517097.99999999,["x"]=-197807.42857143,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -Kobuleti={ -PointsBoundary={ -[1]={["y"]=634427.71428571,["x"]=-318290.28571429,}, -[2]={["y"]=635033.42857143,["x"]=-317550.2857143,}, -[3]={["y"]=635864.85714286,["x"]=-317333.14285715,}, -[4]={["y"]=636967.71428571,["x"]=-317261.71428572,}, -[5]={["y"]=637144.85714286,["x"]=-317913.14285715,}, -[6]={["y"]=634630.57142857,["x"]=-318687.42857144,}, -[7]={["y"]=634424.85714286,["x"]=-318290.2857143,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=634509.71428571,["x"]=-318339.42857144,}, -[2]={["y"]=636767.42857143,["x"]=-317516.57142858,}, -[3]={["y"]=636790,["x"]=-317575.71428572,}, -[4]={["y"]=634531.42857143,["x"]=-318398.00000001,}, -[5]={["y"]=634510.28571429,["x"]=-318339.71428572,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -KrasnodarCenter={ -PointsBoundary={ -[1]={["y"]=366680.28571429,["x"]=11699.142857142,}, -[2]={["y"]=366654.28571429,["x"]=11225.142857142,}, -[3]={["y"]=367497.14285715,["x"]=11082.285714285,}, -[4]={["y"]=368025.71428572,["x"]=10396.57142857,}, -[5]={["y"]=369854.28571429,["x"]=11367.999999999,}, -[6]={["y"]=369840.00000001,["x"]=11910.857142856,}, -[7]={["y"]=366682.57142858,["x"]=11697.999999999,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=369205.42857144,["x"]=11789.142857142,}, -[2]={["y"]=369209.71428572,["x"]=11714.857142856,}, -[3]={["y"]=366699.71428572,["x"]=11581.714285713,}, -[4]={["y"]=366698.28571429,["x"]=11659.142857142,}, -[5]={["y"]=369208.85714286,["x"]=11788.57142857,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -KrasnodarPashkovsky={ -PointsBoundary={ -[1]={["y"]=386754,["x"]=6476.5714285703,}, -[2]={["y"]=389182.57142858,["x"]=8722.2857142846,}, -[3]={["y"]=388832.57142858,["x"]=9086.5714285703,}, -[4]={["y"]=386961.14285715,["x"]=7707.9999999989,}, -[5]={["y"]=385404,["x"]=9179.4285714274,}, -[6]={["y"]=383239.71428572,["x"]=7386.5714285703,}, -[7]={["y"]=383954,["x"]=6486.5714285703,}, -[8]={["y"]=385775.42857143,["x"]=8097.9999999989,}, -[9]={["y"]=386804,["x"]=7319.4285714274,}, -[10]={["y"]=386375.42857143,["x"]=6797.9999999989,}, -[11]={["y"]=386746.85714286,["x"]=6472.2857142846,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=385891.14285715,["x"]=8416.5714285703,}, -[2]={["y"]=385842.28571429,["x"]=8467.9999999989,}, -[3]={["y"]=384180.85714286,["x"]=6917.1428571417,}, -[4]={["y"]=384228.57142858,["x"]=6867.7142857132,}, -[5]={["y"]=385891.14285715,["x"]=8416.5714285703,}, -}, -[2]={ -[1]={["y"]=386714.85714286,["x"]=6674.857142856,}, -[2]={["y"]=386757.71428572,["x"]=6627.7142857132,}, -[3]={["y"]=389028.57142858,["x"]=8741.4285714275,}, -[4]={["y"]=388981.71428572,["x"]=8790.5714285703,}, -[5]={["y"]=386714.57142858,["x"]=6674.5714285703,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -Krymsk={ -PointsBoundary={ -[1]={["y"]=293338.00000001,["x"]=-7575.4285714297,}, -[2]={["y"]=295199.42857144,["x"]=-5434.0000000011,}, -[3]={["y"]=295595.14285715,["x"]=-6239.7142857154,}, -[4]={["y"]=294152.2857143,["x"]=-8325.4285714297,}, -[5]={["y"]=293345.14285715,["x"]=-7596.8571428582,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=293522.00000001,["x"]=-7567.4285714297,}, -[2]={["y"]=293578.57142858,["x"]=-7616.0000000011,}, -[3]={["y"]=295246.00000001,["x"]=-5591.142857144,}, -[4]={["y"]=295187.71428573,["x"]=-5546.0000000011,}, -[5]={["y"]=293523.14285715,["x"]=-7568.2857142868,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -Kutaisi={ -PointsBoundary={ -[1]={["y"]=682087.42857143,["x"]=-284512.85714286,}, -[2]={["y"]=685387.42857143,["x"]=-283662.85714286,}, -[3]={["y"]=685294.57142857,["x"]=-284977.14285715,}, -[4]={["y"]=682744.57142857,["x"]=-286505.71428572,}, -[5]={["y"]=682094.57142857,["x"]=-284527.14285715,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=682638,["x"]=-285202.28571429,}, -[2]={["y"]=685050.28571429,["x"]=-284507.42857144,}, -[3]={["y"]=685068.85714286,["x"]=-284578.85714286,}, -[4]={["y"]=682657.42857143,["x"]=-285264.28571429,}, -[5]={["y"]=682638.28571429,["x"]=-285202.85714286,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -MaykopKhanskaya={ -PointsBoundary={ -[1]={["y"]=456876.28571429,["x"]=-27665.42857143,}, -[2]={["y"]=457800,["x"]=-28392.857142858,}, -[3]={["y"]=459368.57142857,["x"]=-26378.571428573,}, -[4]={["y"]=459425.71428572,["x"]=-25242.857142858,}, -[5]={["y"]=458961.42857143,["x"]=-24964.285714287,}, -[6]={["y"]=456878.57142857,["x"]=-27667.714285715,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=457005.42857143,["x"]=-27668.000000001,}, -[2]={["y"]=459028.85714286,["x"]=-25168.857142858,}, -[3]={["y"]=459082.57142857,["x"]=-25216.857142858,}, -[4]={["y"]=457060,["x"]=-27714.285714287,}, -[5]={["y"]=457004.57142857,["x"]=-27669.714285715,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -MineralnyeVody={ -PointsBoundary={ -[1]={["y"]=703857.14285714,["x"]=-50226.000000002,}, -[2]={["y"]=707385.71428571,["x"]=-51911.714285716,}, -[3]={["y"]=707595.71428571,["x"]=-51434.857142859,}, -[4]={["y"]=707900,["x"]=-51568.857142859,}, -[5]={["y"]=707542.85714286,["x"]=-52326.000000002,}, -[6]={["y"]=706628.57142857,["x"]=-52568.857142859,}, -[7]={["y"]=705142.85714286,["x"]=-51790.285714288,}, -[8]={["y"]=703678.57142857,["x"]=-50611.714285716,}, -[9]={["y"]=703857.42857143,["x"]=-50226.857142859,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=703904,["x"]=-50352.571428573,}, -[2]={["y"]=707596.28571429,["x"]=-52094.571428573,}, -[3]={["y"]=707560.57142858,["x"]=-52161.714285716,}, -[4]={["y"]=703871.71428572,["x"]=-50420.571428573,}, -[5]={["y"]=703902,["x"]=-50352.000000002,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -Mozdok={ -PointsBoundary={ -[1]={["y"]=832123.42857143,["x"]=-83608.571428573,}, -[2]={["y"]=835916.28571429,["x"]=-83144.285714288,}, -[3]={["y"]=835474.28571429,["x"]=-84170.571428573,}, -[4]={["y"]=832911.42857143,["x"]=-84470.571428573,}, -[5]={["y"]=832487.71428572,["x"]=-85565.714285716,}, -[6]={["y"]=831573.42857143,["x"]=-85351.42857143,}, -[7]={["y"]=832123.71428572,["x"]=-83610.285714288,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=832201.14285715,["x"]=-83699.428571431,}, -[2]={["y"]=832212.57142857,["x"]=-83780.571428574,}, -[3]={["y"]=835730.28571429,["x"]=-83335.714285717,}, -[4]={["y"]=835718.85714286,["x"]=-83246.571428574,}, -[5]={["y"]=832200.57142857,["x"]=-83700.000000002,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -Nalchik={ -PointsBoundary={ -[1]={["y"]=759370,["x"]=-125502.85714286,}, -[2]={["y"]=761384.28571429,["x"]=-124177.14285714,}, -[3]={["y"]=761472.85714286,["x"]=-124325.71428572,}, -[4]={["y"]=761092.85714286,["x"]=-125048.57142857,}, -[5]={["y"]=760295.71428572,["x"]=-125685.71428572,}, -[6]={["y"]=759444.28571429,["x"]=-125734.28571429,}, -[7]={["y"]=759375.71428572,["x"]=-125511.42857143,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=759454.28571429,["x"]=-125551.42857143,}, -[2]={["y"]=759492.85714286,["x"]=-125610.85714286,}, -[3]={["y"]=761406.28571429,["x"]=-124304.28571429,}, -[4]={["y"]=761361.14285714,["x"]=-124239.71428572,}, -[5]={["y"]=759456,["x"]=-125552.57142857,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -Novorossiysk={ -PointsBoundary={ -[1]={["y"]=278677.71428573,["x"]=-41656.571428572,}, -[2]={["y"]=278446.2857143,["x"]=-41453.714285715,}, -[3]={["y"]=278989.14285716,["x"]=-40188.000000001,}, -[4]={["y"]=279717.71428573,["x"]=-39968.000000001,}, -[5]={["y"]=280020.57142859,["x"]=-40208.000000001,}, -[6]={["y"]=278674.85714287,["x"]=-41660.857142858,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=278673.14285716,["x"]=-41615.142857144,}, -[2]={["y"]=278625.42857144,["x"]=-41570.571428572,}, -[3]={["y"]=279835.42857144,["x"]=-40226.000000001,}, -[4]={["y"]=279882.2857143,["x"]=-40270.000000001,}, -[5]={["y"]=278672.00000001,["x"]=-41614.857142858,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -SenakiKolkhi={ -PointsBoundary={ -[1]={["y"]=646036.57142857,["x"]=-281778.85714286,}, -[2]={["y"]=646045.14285714,["x"]=-281191.71428571,}, -[3]={["y"]=647032.28571429,["x"]=-280598.85714285,}, -[4]={["y"]=647669.42857143,["x"]=-281273.14285714,}, -[5]={["y"]=648323.71428571,["x"]=-281370.28571428,}, -[6]={["y"]=648520.85714286,["x"]=-281978.85714285,}, -[7]={["y"]=646039.42857143,["x"]=-281783.14285714,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=646060.85714285,["x"]=-281736,}, -[2]={["y"]=646056.57142857,["x"]=-281631.71428571,}, -[3]={["y"]=648442.28571428,["x"]=-281840.28571428,}, -[4]={["y"]=648432.28571428,["x"]=-281918.85714286,}, -[5]={["y"]=646063.71428571,["x"]=-281738.85714286,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -SochiAdler={ -PointsBoundary={ -[1]={["y"]=460642.28571428,["x"]=-164861.71428571,}, -[2]={["y"]=462820.85714285,["x"]=-163368.85714286,}, -[3]={["y"]=463649.42857142,["x"]=-163340.28571429,}, -[4]={["y"]=463835.14285714,["x"]=-164040.28571429,}, -[5]={["y"]=462535.14285714,["x"]=-165654.57142857,}, -[6]={["y"]=460678,["x"]=-165247.42857143,}, -[7]={["y"]=460635.14285714,["x"]=-164876,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=460831.42857143,["x"]=-165180,}, -[2]={["y"]=460878.57142857,["x"]=-165257.14285714,}, -[3]={["y"]=463663.71428571,["x"]=-163793.14285714,}, -[4]={["y"]=463612.28571428,["x"]=-163697.42857143,}, -[5]={["y"]=460831.42857143,["x"]=-165177.14285714,}, -}, -[2]={ -[1]={["y"]=460831.42857143,["x"]=-165180,}, -[2]={["y"]=460878.57142857,["x"]=-165257.14285714,}, -[3]={["y"]=463663.71428571,["x"]=-163793.14285714,}, -[4]={["y"]=463612.28571428,["x"]=-163697.42857143,}, -[5]={["y"]=460831.42857143,["x"]=-165177.14285714,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -Soganlug={ -PointsBoundary={ -[1]={["y"]=894530.85714286,["x"]=-316928.28571428,}, -[2]={["y"]=896422.28571428,["x"]=-318622.57142857,}, -[3]={["y"]=896090.85714286,["x"]=-318934,}, -[4]={["y"]=894019.42857143,["x"]=-317119.71428571,}, -[5]={["y"]=894533.71428571,["x"]=-316925.42857143,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=894525.71428571,["x"]=-316964,}, -[2]={["y"]=896363.14285714,["x"]=-318634.28571428,}, -[3]={["y"]=896299.14285714,["x"]=-318702.85714286,}, -[4]={["y"]=894464,["x"]=-317031.71428571,}, -[5]={["y"]=894524.57142857,["x"]=-316963.71428571,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -SukhumiBabushara={ -PointsBoundary={ -[1]={["y"]=562541.14285714,["x"]=-219852.28571429,}, -[2]={["y"]=562691.14285714,["x"]=-219395.14285714,}, -[3]={["y"]=564326.85714286,["x"]=-219523.71428571,}, -[4]={["y"]=566262.57142857,["x"]=-221166.57142857,}, -[5]={["y"]=566069.71428571,["x"]=-221580.85714286,}, -[6]={["y"]=562534,["x"]=-219873.71428571,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=562684,["x"]=-219779.71428571,}, -[2]={["y"]=562717.71428571,["x"]=-219718,}, -[3]={["y"]=566046.85714286,["x"]=-221376.57142857,}, -[4]={["y"]=566012.28571428,["x"]=-221446.57142857,}, -[5]={["y"]=562684.57142857,["x"]=-219782.57142857,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -TbilisiLochini={ -PointsBoundary={ -[1]={["y"]=895172.85714286,["x"]=-314667.42857143,}, -[2]={["y"]=895337.42857143,["x"]=-314143.14285714,}, -[3]={["y"]=895990.28571429,["x"]=-314036,}, -[4]={["y"]=897730.28571429,["x"]=-315284.57142857,}, -[5]={["y"]=897901.71428571,["x"]=-316284.57142857,}, -[6]={["y"]=897684.57142857,["x"]=-316618.85714286,}, -[7]={["y"]=895173.14285714,["x"]=-314667.42857143,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=895261.14285715,["x"]=-314652.28571428,}, -[2]={["y"]=897654.57142857,["x"]=-316523.14285714,}, -[3]={["y"]=897711.71428571,["x"]=-316450.28571429,}, -[4]={["y"]=895327.42857143,["x"]=-314568.85714286,}, -[5]={["y"]=895261.71428572,["x"]=-314656,}, -}, -[2]={ -[1]={["y"]=895605.71428572,["x"]=-314724.57142857,}, -[2]={["y"]=897639.71428572,["x"]=-316148,}, -[3]={["y"]=897683.42857143,["x"]=-316087.14285714,}, -[4]={["y"]=895650,["x"]=-314660,}, -[5]={["y"]=895606,["x"]=-314724.85714286,} -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -Vaziani={ -PointsBoundary={ -[1]={["y"]=902122,["x"]=-318163.71428572,}, -[2]={["y"]=902678.57142857,["x"]=-317594,}, -[3]={["y"]=903275.71428571,["x"]=-317405.42857143,}, -[4]={["y"]=903418.57142857,["x"]=-317891.14285714,}, -[5]={["y"]=904292.85714286,["x"]=-318748.28571429,}, -[6]={["y"]=904542,["x"]=-319740.85714286,}, -[7]={["y"]=904042,["x"]=-320166.57142857,}, -[8]={["y"]=902121.42857143,["x"]=-318164.85714286,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=902239.14285714,["x"]=-318190.85714286,}, -[2]={["y"]=904014.28571428,["x"]=-319994.57142857,}, -[3]={["y"]=904064.85714285,["x"]=-319945.14285715,}, -[4]={["y"]=902294.57142857,["x"]=-318146,}, -[5]={["y"]=902247.71428571,["x"]=-318190.85714286,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -}, -} -function AIRBASEPOLICE_CAUCASUS:New(SetClient) -local self=BASE:Inherit(self,AIRBASEPOLICE_BASE:New(SetClient,self.Airbases)) -return self -end -AIRBASEPOLICE_NEVADA={ -ClassName="AIRBASEPOLICE_NEVADA", -Airbases={ -Nellis={ -PointsBoundary={ -[1]={["y"]=-17814.714285714,["x"]=-399823.14285714,}, -[2]={["y"]=-16875.857142857,["x"]=-398763.14285714,}, -[3]={["y"]=-16251.571428571,["x"]=-398988.85714286,}, -[4]={["y"]=-16163,["x"]=-398693.14285714,}, -[5]={["y"]=-16328.714285714,["x"]=-398034.57142857,}, -[6]={["y"]=-15943,["x"]=-397571.71428571,}, -[7]={["y"]=-15711.571428571,["x"]=-397551.71428571,}, -[8]={["y"]=-15748.714285714,["x"]=-396806,}, -[9]={["y"]=-16288.714285714,["x"]=-396517.42857143,}, -[10]={["y"]=-16751.571428571,["x"]=-396308.85714286,}, -[11]={["y"]=-17263,["x"]=-396234.57142857,}, -[12]={["y"]=-17577.285714286,["x"]=-396640.28571429,}, -[13]={["y"]=-17614.428571429,["x"]=-397400.28571429,}, -[14]={["y"]=-19405.857142857,["x"]=-399428.85714286,}, -[15]={["y"]=-19234.428571429,["x"]=-399683.14285714,}, -[16]={["y"]=-18708.714285714,["x"]=-399408.85714286,}, -[17]={["y"]=-18397.285714286,["x"]=-399657.42857143,}, -[18]={["y"]=-17814.428571429,["x"]=-399823.42857143,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=-18687,["x"]=-399380.28571429,}, -[2]={["y"]=-18620.714285714,["x"]=-399436.85714286,}, -[3]={["y"]=-16217.857142857,["x"]=-396596.85714286,}, -[4]={["y"]=-16300.142857143,["x"]=-396530,}, -[5]={["y"]=-18687,["x"]=-399380.85714286,}, -}, -[2]={ -[1]={["y"]=-18451.571428572,["x"]=-399580.57142857,}, -[2]={["y"]=-18392.142857143,["x"]=-399628.57142857,}, -[3]={["y"]=-16011,["x"]=-396806.85714286,}, -[4]={["y"]=-16074.714285714,["x"]=-396751.71428572,}, -[5]={["y"]=-18451.571428572,["x"]=-399580.85714285,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -McCarran={ -PointsBoundary={ -[1]={["y"]=-29455.285714286,["x"]=-416277.42857142,}, -[2]={["y"]=-28860.142857143,["x"]=-416492,}, -[3]={["y"]=-25044.428571429,["x"]=-416344.85714285,}, -[4]={["y"]=-24580.142857143,["x"]=-415959.14285714,}, -[5]={["y"]=-25073,["x"]=-415630.57142857,}, -[6]={["y"]=-25087.285714286,["x"]=-415130.57142857,}, -[7]={["y"]=-25830.142857143,["x"]=-414866.28571428,}, -[8]={["y"]=-26658.714285715,["x"]=-414880.57142857,}, -[9]={["y"]=-26973,["x"]=-415273.42857142,}, -[10]={["y"]=-27380.142857143,["x"]=-415187.71428571,}, -[11]={["y"]=-27715.857142857,["x"]=-414144.85714285,}, -[12]={["y"]=-27551.571428572,["x"]=-413473.42857142,}, -[13]={["y"]=-28630.142857143,["x"]=-413201.99999999,}, -[14]={["y"]=-29494.428571429,["x"]=-415437.71428571,}, -[15]={["y"]=-29455.571428572,["x"]=-416277.71428571,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=-29408.428571429,["x"]=-416016.28571428,}, -[2]={["y"]=-29408.142857144,["x"]=-416105.42857142,}, -[3]={["y"]=-24680.714285715,["x"]=-416003.14285713,}, -[4]={["y"]=-24681.857142858,["x"]=-415926.57142856,}, -[5]={["y"]=-29408.42857143,["x"]=-416016.57142856,}, -}, -[2]={ -[1]={["y"]=-28575.571428572,["x"]=-416303.14285713,}, -[2]={["y"]=-28575.571428572,["x"]=-416382.57142856,}, -[3]={["y"]=-25111.000000001,["x"]=-416309.7142857,}, -[4]={["y"]=-25111.000000001,["x"]=-416249.14285713,}, -[5]={["y"]=-28575.571428572,["x"]=-416303.7142857,}, -}, -[3]={ -[1]={["y"]=-29331.000000001,["x"]=-416275.42857141,}, -[2]={["y"]=-29259.000000001,["x"]=-416306.85714284,}, -[3]={["y"]=-28005.571428572,["x"]=-413449.7142857,}, -[4]={["y"]=-28068.714285715,["x"]=-413422.85714284,}, -[5]={["y"]=-29331.000000001,["x"]=-416275.7142857,}, -}, -[4]={ -[1]={["y"]=-29073.285714286,["x"]=-416386.57142856,}, -[2]={["y"]=-28997.285714286,["x"]=-416417.42857141,}, -[3]={["y"]=-27697.571428572,["x"]=-413464.57142856,}, -[4]={["y"]=-27767.857142858,["x"]=-413434.28571427,}, -[5]={["y"]=-29073.000000001,["x"]=-416386.85714284,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -Creech={ -PointsBoundary={ -[1]={["y"]=-74522.714285715,["x"]=-360887.99999998,}, -[2]={["y"]=-74197,["x"]=-360556.57142855,}, -[3]={["y"]=-74402.714285715,["x"]=-359639.42857141,}, -[4]={["y"]=-74637,["x"]=-359279.42857141,}, -[5]={["y"]=-75759.857142857,["x"]=-359005.14285712,}, -[6]={["y"]=-75834.142857143,["x"]=-359045.14285712,}, -[7]={["y"]=-75902.714285714,["x"]=-359782.28571427,}, -[8]={["y"]=-76099.857142857,["x"]=-360399.42857141,}, -[9]={["y"]=-77314.142857143,["x"]=-360219.42857141,}, -[10]={["y"]=-77728.428571429,["x"]=-360445.14285713,}, -[11]={["y"]=-77585.571428571,["x"]=-360585.14285713,}, -[12]={["y"]=-76471.285714286,["x"]=-360819.42857141,}, -[13]={["y"]=-76325.571428571,["x"]=-360942.28571427,}, -[14]={["y"]=-74671.857142857,["x"]=-360927.7142857,}, -[15]={["y"]=-74522.714285714,["x"]=-360888.85714284,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=-74237.571428571,["x"]=-360591.7142857,}, -[2]={["y"]=-74234.428571429,["x"]=-360493.71428571,}, -[3]={["y"]=-77605.285714286,["x"]=-360399.14285713,}, -[4]={["y"]=-77608.714285715,["x"]=-360498.85714285,}, -[5]={["y"]=-74237.857142857,["x"]=-360591.7142857,}, -}, -[2]={ -[1]={["y"]=-75807.571428572,["x"]=-359073.42857142,}, -[2]={["y"]=-74770.142857144,["x"]=-360581.71428571,}, -[3]={["y"]=-74641.285714287,["x"]=-360585.42857142,}, -[4]={["y"]=-75734.142857144,["x"]=-359023.14285714,}, -[5]={["y"]=-75807.285714287,["x"]=-359073.42857142,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -GroomLake={ -PointsBoundary={ -[1]={["y"]=-88916.714285714,["x"]=-289102.28571425,}, -[2]={["y"]=-87023.571428572,["x"]=-290388.57142857,}, -[3]={["y"]=-85916.428571429,["x"]=-290674.28571428,}, -[4]={["y"]=-87645.000000001,["x"]=-286567.14285714,}, -[5]={["y"]=-88380.714285715,["x"]=-286388.57142857,}, -[6]={["y"]=-89670.714285715,["x"]=-283524.28571428,}, -[7]={["y"]=-89797.857142858,["x"]=-283567.14285714,}, -[8]={["y"]=-88635.000000001,["x"]=-286749.99999999,}, -[9]={["y"]=-89177.857142858,["x"]=-287207.14285714,}, -[10]={["y"]=-89092.142857144,["x"]=-288892.85714285,}, -[11]={["y"]=-88917.000000001,["x"]=-289102.85714285,}, -}, -PointsRunways={ -[1]={ -[1]={["y"]=-86039.000000001,["x"]=-290606.28571428,}, -[2]={["y"]=-85965.285714287,["x"]=-290573.99999999,}, -[3]={["y"]=-87692.714285715,["x"]=-286634.85714285,}, -[4]={["y"]=-87756.714285715,["x"]=-286663.99999999,}, -[5]={["y"]=-86038.714285715,["x"]=-290606.85714285,}, -}, -[2]={ -[1]={["y"]=-86808.428571429,["x"]=-290375.7142857,}, -[2]={["y"]=-86732.714285715,["x"]=-290344.28571427,}, -[3]={["y"]=-89672.714285714,["x"]=-283546.57142855,}, -[4]={["y"]=-89772.142857143,["x"]=-283587.71428569,}, -[5]={["y"]=-86808.142857143,["x"]=-290375.7142857,}, -}, -}, -ZoneBoundary={}, -ZoneRunways={}, -MaximumSpeed=50, -}, -}, -} -function AIRBASEPOLICE_NEVADA:New(SetClient) -local self=BASE:Inherit(self,AIRBASEPOLICE_BASE:New(SetClient,self.Airbases)) -end -do -DETECTION_BASE={ -ClassName="DETECTION_BASE", -DetectionSetGroup=nil, -DetectionRange=nil, -DetectedObjects={}, -DetectionRun=0, -DetectedObjectsIdentified={}, -DetectedItems={}, -} -function DETECTION_BASE:New(DetectionSetGroup) -local self=BASE:Inherit(self,FSM:New()) -self.DetectedItemCount=0 -self.DetectedItemMax=0 -self.DetectedItems={} -self.DetectionSetGroup=DetectionSetGroup -self.RefreshTimeInterval=30 -self:InitDetectVisual(nil) -self:InitDetectOptical(nil) -self:InitDetectRadar(nil) -self:InitDetectRWR(nil) -self:InitDetectIRST(nil) -self:InitDetectDLINK(nil) -self:FilterCategories({ -Unit.Category.AIRPLANE, -Unit.Category.GROUND_UNIT, -Unit.Category.HELICOPTER, -Unit.Category.SHIP, -Unit.Category.STRUCTURE -}) -self:SetFriendliesRange(6000) -self:SetStartState("Stopped") -self:AddTransition("Stopped","Start","Detecting") -self:AddTransition("Detecting","Detect","Detecting") -self:AddTransition("Detecting","DetectionGroup","Detecting") -self:AddTransition("Detecting","Detected","Detecting") -self:AddTransition("*","Stop","Stopped") -return self -end -do -function DETECTION_BASE:onafterStart(From,Event,To) -self:__Detect(1) -end -function DETECTION_BASE:onafterDetect(From,Event,To) -self:E({From,Event,To}) -local DetectDelay=0.1 -self.DetectionCount=0 -self.DetectionRun=0 -self:UnIdentifyAllDetectedObjects() -local DetectionTimeStamp=timer.getTime() -for DetectionGroupID,DetectionGroupData in pairs(self.DetectionSetGroup:GetSet())do -self:__DetectionGroup(DetectDelay,DetectionGroupData,DetectionTimeStamp) -self.DetectionCount=self.DetectionCount+1 -DetectDelay=DetectDelay+1 -end -end -function DETECTION_BASE:onafterDetectionGroup(From,Event,To,DetectionGroup,DetectionTimeStamp) -self:E({From,Event,To}) -self.DetectionRun=self.DetectionRun+1 -local HasDetectedObjects=false -if DetectionGroup:IsAlive()then -self:T({"DetectionGroup is Alive",DetectionGroup:GetName()}) -local DetectionGroupName=DetectionGroup:GetName() -local DetectionUnit=DetectionGroup:GetUnit(1) -local DetectedUnits={} -local DetectedTargets=DetectionGroup:GetDetectedTargets( -self.DetectVisual, -self.DetectOptical, -self.DetectRadar, -self.DetectIRST, -self.DetectRWR, -self.DetectDLINK -) -self:F(DetectedTargets) -for DetectionObjectID,Detection in pairs(DetectedTargets)do -local DetectedObject=Detection.object -if DetectedObject and DetectedObject:isExist()and DetectedObject.id_<50000000 then -local TargetIsDetected,TargetIsVisible,TargetLastTime,TargetKnowType,TargetKnowDistance,TargetLastPos,TargetLastVelocity=DetectionUnit:IsTargetDetected( -DetectedObject, -self.DetectVisual, -self.DetectOptical, -self.DetectRadar, -self.DetectIRST, -self.DetectRWR, -self.DetectDLINK -) -self:T2({TargetIsDetected=TargetIsDetected,TargetIsVisible=TargetIsVisible,TargetLastTime=TargetLastTime,TargetKnowType=TargetKnowType,TargetKnowDistance=TargetKnowDistance,TargetLastPos=TargetLastPos,TargetLastVelocity=TargetLastVelocity}) -local DetectionAccepted=true -local DetectedObjectName=DetectedObject:getName() -local DetectedObjectType=DetectedObject:getTypeName() -local DetectedObjectVec3=DetectedObject:getPoint() -local DetectedObjectVec2={x=DetectedObjectVec3.x,y=DetectedObjectVec3.z} -local DetectionGroupVec3=DetectionGroup:GetVec3() -local DetectionGroupVec2={x=DetectionGroupVec3.x,y=DetectionGroupVec3.z} -local Distance=((DetectedObjectVec3.x-DetectionGroupVec3.x)^2+ -(DetectedObjectVec3.y-DetectionGroupVec3.y)^2+ -(DetectedObjectVec3.z-DetectionGroupVec3.z)^2 -)^0.5/1000 -local DetectedUnitCategory=DetectedObject:getDesc().category -self:F({"Detected Target:",DetectionGroupName,DetectedObjectName,DetectedObjectType,Distance,DetectedUnitCategory}) -DetectionAccepted=self._.FilterCategories[DetectedUnitCategory]~=nil and DetectionAccepted or false -if self.AcceptRange and Distance>self.AcceptRange then -DetectionAccepted=false -end -if self.AcceptZones then -for AcceptZoneID,AcceptZone in pairs(self.AcceptZones)do -local AcceptZone=AcceptZone -if AcceptZone:IsVec2InZone(DetectedObjectVec2)==false then -DetectionAccepted=false -end -end -end -if self.RejectZones then -for RejectZoneID,RejectZone in pairs(self.RejectZones)do -local RejectZone=RejectZone -if RejectZone:IsPointVec2InZone(DetectedObjectVec2)==true then -DetectionAccepted=false -end -end -end -if not self.DetectedObjects[DetectedObjectName]and Detection.visible and self.DistanceProbability then -local DistanceFactor=Distance/4 -local DistanceProbabilityReversed=(1-self.DistanceProbability)*DistanceFactor -local DistanceProbability=1-DistanceProbabilityReversed -DistanceProbability=DistanceProbability*30/300 -local Probability=math.random() -self:T({Probability,DistanceProbability}) -if Probability>DistanceProbability then -DetectionAccepted=false -end -end -if not self.DetectedObjects[DetectedObjectName]and Detection.visible and self.AlphaAngleProbability then -local NormalVec2={x=DetectedObjectVec2.x-DetectionGroupVec2.x,y=DetectedObjectVec2.y-DetectionGroupVec2.y} -local AlphaAngle=math.atan2(NormalVec2.y,NormalVec2.x) -local Sinus=math.sin(AlphaAngle) -local AlphaAngleProbabilityReversed=(1-self.AlphaAngleProbability)*(1-Sinus) -local AlphaAngleProbability=1-AlphaAngleProbabilityReversed -AlphaAngleProbability=AlphaAngleProbability*30/300 -local Probability=math.random() -self:T({Probability,AlphaAngleProbability}) -if Probability>AlphaAngleProbability then -DetectionAccepted=false -end -end -if not self.DetectedObjects[DetectedObjectName]and Detection.visible and self.ZoneProbability then -for ZoneDataID,ZoneData in pairs(self.ZoneProbability)do -self:E({ZoneData}) -local ZoneObject=ZoneData[1] -local ZoneProbability=ZoneData[2] -ZoneProbability=ZoneProbability*30/300 -if ZoneObject:IsPointVec2InZone(DetectedObjectVec2)==true then -local Probability=math.random() -self:T({Probability,ZoneProbability}) -if Probability>ZoneProbability then -DetectionAccepted=false -break -end -end -end -end -if DetectionAccepted then -HasDetectedObjects=true -self.DetectedObjects[DetectedObjectName]=self.DetectedObjects[DetectedObjectName]or{} -self.DetectedObjects[DetectedObjectName].Name=DetectedObjectName -self.DetectedObjects[DetectedObjectName].IsDetected=TargetIsDetected -self.DetectedObjects[DetectedObjectName].IsVisible=TargetIsVisible -self.DetectedObjects[DetectedObjectName].LastTime=TargetLastTime -self.DetectedObjects[DetectedObjectName].LastPos=TargetLastPos -self.DetectedObjects[DetectedObjectName].LastVelocity=TargetLastVelocity -self.DetectedObjects[DetectedObjectName].KnowType=TargetKnowType -self.DetectedObjects[DetectedObjectName].KnowDistance=Detection.distance -self.DetectedObjects[DetectedObjectName].Distance=Distance -self.DetectedObjects[DetectedObjectName].DetectionTimeStamp=DetectionTimeStamp -self:F({DetectedObject=self.DetectedObjects[DetectedObjectName]}) -local DetectedUnit=UNIT:FindByName(DetectedObjectName) -DetectedUnits[DetectedObjectName]=DetectedUnit -else -if self.DetectedObjects[DetectedObjectName]then -self.DetectedObjects[DetectedObjectName]=nil -end -end -end -self:T2(self.DetectedObjects) -end -if HasDetectedObjects then -self:__Detected(0.1,DetectedUnits) -end -end -if self.DetectionCount>0 and self.DetectionRun==self.DetectionCount then -self:T("--> Create Detection Sets") -for DetectedObjectName,DetectedObject in pairs(self.DetectedObjects)do -if self.DetectedObjects[DetectedObjectName].IsDetected==true and self.DetectedObjects[DetectedObjectName].DetectionTimeStamp+60<=DetectionTimeStamp then -self.DetectedObjects[DetectedObjectName].IsDetected=false -end -end -self:CreateDetectionItems() -for DetectedItemID,DetectedItem in pairs(self.DetectedItems)do -self:UpdateDetectedItemDetection(DetectedItem) -self:CleanDetectionItem(DetectedItem,DetectedItemID) -end -self:__Detect(self.RefreshTimeInterval) -end -end -end -do -function DETECTION_BASE:CleanDetectionItem(DetectedItem,DetectedItemID) -self:F2() -local DetectedSet=DetectedItem.Set -if DetectedSet:Count()==0 then -self:RemoveDetectedItem(DetectedItemID) -end -return self -end -function DETECTION_BASE:ForgetDetectedUnit(UnitName) -self:F2() -local DetectedItems=self:GetDetectedItems() -for DetectedItemIndex,DetectedItem in pairs(DetectedItems)do -local DetectedSet=self:GetDetectedSet(DetectedItemIndex) -if DetectedSet then -DetectedSet:RemoveUnitsByName(UnitName) -end -end -return self -end -function DETECTION_BASE:CreateDetectionItems() -self:F2() -self:E("Error, in DETECTION_BASE class...") -return self -end -end -do -function DETECTION_BASE:InitDetectVisual(DetectVisual) -self.DetectVisual=DetectVisual -return self -end -function DETECTION_BASE:InitDetectOptical(DetectOptical) -self:F2() -self.DetectOptical=DetectOptical -return self -end -function DETECTION_BASE:InitDetectRadar(DetectRadar) -self:F2() -self.DetectRadar=DetectRadar -return self -end -function DETECTION_BASE:InitDetectIRST(DetectIRST) -self:F2() -self.DetectIRST=DetectIRST -return self -end -function DETECTION_BASE:InitDetectRWR(DetectRWR) -self:F2() -self.DetectRWR=DetectRWR -return self -end -function DETECTION_BASE:InitDetectDLINK(DetectDLINK) -self:F2() -self.DetectDLINK=DetectDLINK -return self -end -end -do -function DETECTION_BASE:FilterCategories(FilterCategories) -self:F2() -self._.FilterCategories={} -if type(FilterCategories)=="table"then -for CategoryID,Category in pairs(FilterCategories)do -self._.FilterCategories[Category]=Category -end -else -self._.FilterCategories[FilterCategories]=FilterCategories -end -return self -end -end -do -function DETECTION_BASE:SetRefreshTimeInterval(RefreshTimeInterval) -self:F2() -self.RefreshTimeInterval=RefreshTimeInterval -return self -end -end -do -function DETECTION_BASE:SetFriendliesRange(FriendliesRange) -self:F2() -self.FriendliesRange=FriendliesRange -return self -end -end -do -function DETECTION_BASE:SetIntercept(Intercept,InterceptDelay) -self:F2() -self.Intercept=Intercept -self.InterceptDelay=InterceptDelay -return self -end -end -do -function DETECTION_BASE:SetAcceptRange(AcceptRange) -self:F2() -self.AcceptRange=AcceptRange -return self -end -function DETECTION_BASE:SetAcceptZones(AcceptZones) -self:F2() -if type(AcceptZones)=="table"then -if AcceptZones.ClassName and AcceptZones:IsInstanceOf(ZONE_BASE)then -self.AcceptZones={AcceptZones} -else -self.AcceptZones=AcceptZones -end -else -self:E({"AcceptZones must be a list of ZONE_BASE derived objects or one ZONE_BASE derived object",AcceptZones}) -error() -end -return self -end -function DETECTION_BASE:SetRejectZones(RejectZones) -self:F2() -if type(RejectZones)=="table"then -if RejectZones.ClassName and RejectZones:IsInstanceOf(ZONE_BASE)then -self.RejectZones={RejectZones} -else -self.RejectZones=RejectZones -end -else -self:E({"RejectZones must be a list of ZONE_BASE derived objects or one ZONE_BASE derived object",RejectZones}) -error() -end -return self -end -end -do -function DETECTION_BASE:SetDistanceProbability(DistanceProbability) -self:F2() -self.DistanceProbability=DistanceProbability -return self -end -function DETECTION_BASE:SetAlphaAngleProbability(AlphaAngleProbability) -self:F2() -self.AlphaAngleProbability=AlphaAngleProbability -return self -end -function DETECTION_BASE:SetZoneProbability(ZoneArray) -self:F2() -self.ZoneProbability=ZoneArray -return self -end -end -do -function DETECTION_BASE:AcceptChanges(DetectedItem) -DetectedItem.Changed=false -DetectedItem.Changes={} -return self -end -function DETECTION_BASE:AddChangeItem(DetectedItem,ChangeCode,ItemUnitType) -DetectedItem.Changed=true -local ID=DetectedItem.ID -DetectedItem.Changes=DetectedItem.Changes or{} -DetectedItem.Changes[ChangeCode]=DetectedItem.Changes[ChangeCode]or{} -DetectedItem.Changes[ChangeCode].ID=ID -DetectedItem.Changes[ChangeCode].ItemUnitType=ItemUnitType -self:E({"Change on Detection Item:",DetectedItem.ID,ChangeCode,ItemUnitType}) -return self -end -function DETECTION_BASE:AddChangeUnit(DetectedItem,ChangeCode,ChangeUnitType) -DetectedItem.Changed=true -local ID=DetectedItem.ID -DetectedItem.Changes=DetectedItem.Changes or{} -DetectedItem.Changes[ChangeCode]=DetectedItem.Changes[ChangeCode]or{} -DetectedItem.Changes[ChangeCode][ChangeUnitType]=DetectedItem.Changes[ChangeCode][ChangeUnitType]or 0 -DetectedItem.Changes[ChangeCode][ChangeUnitType]=DetectedItem.Changes[ChangeCode][ChangeUnitType]+1 -DetectedItem.Changes[ChangeCode].ID=ID -self:E({"Change on Detection Item:",DetectedItem.ID,ChangeCode,ChangeUnitType}) -return self -end -end -do -function DETECTION_BASE:SetFriendlyPrefixes(FriendlyPrefixes) -self.FriendlyPrefixes=self.FriendlyPrefixes or{} -if type(FriendlyPrefixes)~="table"then -FriendlyPrefixes={FriendlyPrefixes} -end -for PrefixID,Prefix in pairs(FriendlyPrefixes)do -self:F({FriendlyPrefix=Prefix}) -self.FriendlyPrefixes[Prefix]=Prefix -end -return self -end -function DETECTION_BASE:IsFriendliesNearBy(DetectedItem) -return DetectedItem.FriendliesNearBy~=nil or false -end -function DETECTION_BASE:GetFriendliesNearBy(DetectedItem) -return DetectedItem.FriendliesNearBy -end -function DETECTION_BASE:FilterFriendliesCategory(FriendliesCategory) -self.FriendliesCategory=FriendliesCategory -return self -end -function DETECTION_BASE:IsFriendliesNearIntercept(DetectedItem) -return DetectedItem.FriendliesNearIntercept~=nil or false -end -function DETECTION_BASE:GetFriendliesNearIntercept(DetectedItem) -return DetectedItem.FriendliesNearIntercept -end -function DETECTION_BASE:GetFriendliesDistance(DetectedItem) -return DetectedItem.FriendliesDistance -end -function DETECTION_BASE:IsPlayersNearBy(DetectedItem) -return DetectedItem.PlayersNearBy~=nil -end -function DETECTION_BASE:GetPlayersNearBy(DetectedItem) -return DetectedItem.PlayersNearBy -end -function DETECTION_BASE:ReportFriendliesNearBy(ReportGroupData) -self:F2() -local DetectedItem=ReportGroupData.DetectedItem -local DetectedSet=ReportGroupData.DetectedItem.Set -local DetectedUnit=DetectedSet:GetFirst() -DetectedItem.FriendliesNearBy=nil -if DetectedUnit and DetectedUnit:IsAlive()then -local DetectedUnitCoord=DetectedUnit:GetCoordinate() -local InterceptCoord=ReportGroupData.InterceptCoord or DetectedUnitCoord -local SphereSearch={ -id=world.VolumeType.SPHERE, -params={ -point=InterceptCoord:GetVec3(), -radius=self.FriendliesRange, -} -} -local FindNearByFriendlies=function(FoundDCSUnit,ReportGroupData) -local DetectedItem=ReportGroupData.DetectedItem -local DetectedSet=ReportGroupData.DetectedItem.Set -local DetectedUnit=DetectedSet:GetFirst() -local DetectedUnitCoord=DetectedUnit:GetCoordinate() -local InterceptCoord=ReportGroupData.InterceptCoord or DetectedUnitCoord -local ReportSetGroup=ReportGroupData.ReportSetGroup -local EnemyCoalition=DetectedUnit:GetCoalition() -local FoundUnitCoalition=FoundDCSUnit:getCoalition() -local FoundUnitName=FoundDCSUnit:getName() -local FoundUnitGroupName=FoundDCSUnit:getGroup():getName() -local EnemyUnitName=DetectedUnit:GetName() -local FoundUnitInReportSetGroup=ReportSetGroup:FindGroup(FoundUnitGroupName)~=nil -self:T({"Friendlies search:",FoundUnitName,FoundUnitCoalition,EnemyUnitName,EnemyCoalition,FoundUnitInReportSetGroup}) -if FoundUnitInReportSetGroup==true then -for PrefixID,Prefix in pairs(self.FriendlyPrefixes or{})do -self:F({"FriendlyPrefix:",Prefix}) -if string.find(FoundUnitName,Prefix:gsub("-","%%-"),1)then -FoundUnitInReportSetGroup=false -break -end -end -end -self:F({"Friendlies search:",FoundUnitName,FoundUnitCoalition,EnemyUnitName,EnemyCoalition,FoundUnitInReportSetGroup}) -if FoundUnitCoalition~=EnemyCoalition and FoundUnitInReportSetGroup==false then -local FriendlyUnit=UNIT:Find(FoundDCSUnit) -local FriendlyUnitName=FriendlyUnit:GetName() -local FriendlyUnitCategory=FriendlyUnit:GetDesc().category -self:T({FriendlyUnitCategory=FriendlyUnitCategory,FriendliesCategory=self.FriendliesCategory}) -DetectedItem.FriendliesNearBy=DetectedItem.FriendliesNearBy or{} -DetectedItem.FriendliesNearBy[FriendlyUnitName]=FriendlyUnit -local Distance=DetectedUnitCoord:Get2DDistance(FriendlyUnit:GetCoordinate()) -DetectedItem.FriendliesDistance=DetectedItem.FriendliesDistance or{} -DetectedItem.FriendliesDistance[Distance]=FriendlyUnit -self:T({FriendlyUnitName=FriendlyUnitName,Distance=Distance}) -return true -end -return true -end -world.searchObjects(Object.Category.UNIT,SphereSearch,FindNearByFriendlies,ReportGroupData) -DetectedItem.PlayersNearBy=nil -local DetectionZone=ZONE_UNIT:New("DetectionPlayers",DetectedUnit,self.FriendliesRange) -_DATABASE:ForEachPlayer( -function(PlayerUnitName) -local PlayerUnit=UNIT:FindByName(PlayerUnitName) -if PlayerUnit and PlayerUnit:IsInZone(DetectionZone)then -local PlayerUnitCategory=PlayerUnit:GetDesc().category -if(not self.FriendliesCategory)or(self.FriendliesCategory and(self.FriendliesCategory==PlayerUnitCategory))then -DetectedItem.FriendliesNearBy=DetectedItem.FriendliesNearBy or{} -local PlayerUnitName=PlayerUnit:GetName() -DetectedItem.PlayersNearBy=DetectedItem.PlayersNearBy or{} -DetectedItem.PlayersNearBy[PlayerUnitName]=PlayerUnit -DetectedItem.FriendliesNearBy=DetectedItem.FriendliesNearBy or{} -DetectedItem.FriendliesNearBy[PlayerUnitName]=PlayerUnit -local Distance=DetectedUnitCoord:Get2DDistance(PlayerUnit:GetCoordinate()) -DetectedItem.FriendliesDistance=DetectedItem.FriendliesDistance or{} -DetectedItem.FriendliesDistance[Distance]=PlayerUnit -end -end -end -) -end -end -end -function DETECTION_BASE:IsDetectedObjectIdentified(DetectedObject) -local DetectedObjectName=DetectedObject.Name -if DetectedObjectName then -local DetectedObjectIdentified=self.DetectedObjectsIdentified[DetectedObjectName]==true -self:T3(DetectedObjectIdentified) -return DetectedObjectIdentified -else -return nil -end -end -function DETECTION_BASE:IdentifyDetectedObject(DetectedObject) -local DetectedObjectName=DetectedObject.Name -self.DetectedObjectsIdentified[DetectedObjectName]=true -end -function DETECTION_BASE:UnIdentifyDetectedObject(DetectedObject) -local DetectedObjectName=DetectedObject.Name -self.DetectedObjectsIdentified[DetectedObjectName]=false -end -function DETECTION_BASE:UnIdentifyAllDetectedObjects() -self.DetectedObjectsIdentified={} -end -function DETECTION_BASE:GetDetectedObject(ObjectName) -if ObjectName then -local DetectedObject=self.DetectedObjects[ObjectName] -if DetectedObject then -local DetectedUnit=UNIT:FindByName(ObjectName) -if DetectedUnit and DetectedUnit:IsAlive()then -if self:IsDetectedObjectIdentified(DetectedObject)==false then -return DetectedObject -end -end -end -end -return nil -end -function DETECTION_BASE:GetDetectedUnitTypeName(DetectedUnit) -if DetectedUnit and DetectedUnit:IsAlive()then -local DetectedUnitName=DetectedUnit:GetName() -local DetectedObject=self.DetectedObjects[DetectedUnitName] -if DetectedObject then -if DetectedObject.KnowType then -return DetectedUnit:GetTypeName() -else -return"Unknown" -end -else -return"Unknown" -end -else -return"Dead:"..DetectedUnit:GetName() -end -return"Undetected:"..DetectedUnit:GetName() -end -function DETECTION_BASE:AddDetectedItem(ItemPrefix,DetectedItemIndex,Set) -local DetectedItem={} -self.DetectedItemCount=self.DetectedItemCount+1 -self.DetectedItemMax=self.DetectedItemMax+1 -if DetectedItemIndex then -self.DetectedItems[DetectedItemIndex]=DetectedItem -else -self.DetectedItems[self.DetectedItemCount]=DetectedItem -end -DetectedItem.Set=Set or SET_UNIT:New():FilterDeads():FilterCrashes() -DetectedItem.Index=DetectedItemIndex or self.DetectedItemCount -DetectedItem.ItemID=ItemPrefix.."."..self.DetectedItemMax -DetectedItem.ID=self.DetectedItemMax -DetectedItem.Removed=false -return DetectedItem -end -function DETECTION_BASE:AddDetectedItemZone(DetectedItemIndex,Set,Zone) -local DetectedItem=self:AddDetectedItem("AREA",DetectedItemIndex,Set) -DetectedItem.Zone=Zone -return DetectedItem -end -function DETECTION_BASE:RemoveDetectedItem(DetectedItemIndex) -if self.DetectedItems[DetectedItemIndex]then -self.DetectedItemCount=self.DetectedItemCount-1 -self.DetectedItems[DetectedItemIndex]=nil -end -end -function DETECTION_BASE:GetDetectedItems() -return self.DetectedItems -end -function DETECTION_BASE:GetDetectedItemsCount() -local DetectedCount=self.DetectedItemCount -return DetectedCount -end -function DETECTION_BASE:GetDetectedItem(Index) -local DetectedItem=self.DetectedItems[Index] -if DetectedItem then -return DetectedItem -end -return nil -end -function DETECTION_BASE:GetDetectedItemID(Index) -local DetectedItem=self.DetectedItems[Index] -if DetectedItem then -return DetectedItem.ItemID -end -return"" -end -function DETECTION_BASE:GetDetectedID(Index) -local DetectedItem=self.DetectedItems[Index] -if DetectedItem then -return DetectedItem.ID -end -return"" -end -function DETECTION_BASE:GetDetectedSet(Index) -local DetectedItem=self:GetDetectedItem(Index) -local DetectedSetUnit=DetectedItem.Set -if DetectedSetUnit then -return DetectedSetUnit -end -return nil -end -function DETECTION_BASE:UpdateDetectedItemDetection(DetectedItem) -local IsDetected=false -for UnitName,UnitData in pairs(DetectedItem.Set:GetSet())do -local DetectedObject=self.DetectedObjects[UnitName] -self:F({UnitName=UnitName,IsDetected=DetectedObject.IsDetected}) -if DetectedObject.IsDetected then -IsDetected=true -break -end -end -self:F({IsDetected=DetectedItem.IsDetected}) -DetectedItem.IsDetected=IsDetected -return IsDetected -end -function DETECTION_BASE:IsDetectedItemDetected(DetectedItem) -return DetectedItem.IsDetected -end -do -function DETECTION_BASE:GetDetectedItemZone(Index) -local DetectedZone=self.DetectedItems[Index].Zone -if DetectedZone then -return DetectedZone -end -local Detected -return nil -end -end -function DETECTION_BASE:SetDetectedItemCoordinate(DetectedItem,Coordinate,DetectedItemUnit) -self:F({Coordinate=Coordinate}) -if DetectedItem then -if DetectedItemUnit then -DetectedItem.Coordinate=Coordinate -DetectedItem.Coordinate:SetHeading(DetectedItemUnit:GetHeading()) -DetectedItem.Coordinate.y=DetectedItemUnit:GetAltitude() -DetectedItem.Coordinate:SetVelocity(DetectedItemUnit:GetVelocityMPS()) -end -end -end -function DETECTION_BASE:GetDetectedItemCoordinate(Index) -self:F({Index=Index}) -local DetectedItem=self:GetDetectedItem(Index) -if DetectedItem then -return DetectedItem.Coordinate -end -return nil -end -function DETECTION_BASE:SetDetectedItemThreatLevel(DetectedItem) -local DetectedSet=DetectedItem.Set -if DetectedItem then -DetectedItem.ThreatLevel,DetectedItem.ThreatText=DetectedSet:CalculateThreatLevelA2G() -end -end -function DETECTION_BASE:GetDetectedItemThreatLevel(Index) -self:F({Index=Index}) -local DetectedItem=self:GetDetectedItem(Index) -if DetectedItem then -return DetectedItem.ThreatLevel or 0,DetectedItem.ThreatText or"" -end -return nil,"" -end -function DETECTION_BASE:DetectedItemMenu(Index,AttackGroup) -self:F(Index) -return nil -end -function DETECTION_BASE:DetectedItemReportSummary(Index,AttackGroup,Settings) -self:F(Index) -return nil -end -function DETECTION_BASE:DetectedReportDetailed(AttackGroup) -self:F() -return nil -end -function DETECTION_BASE:GetDetectionSetGroup() -local DetectionSetGroup=self.DetectionSetGroup -return DetectionSetGroup -end -function DETECTION_BASE:Schedule(DelayTime,RepeatInterval) -self:F2() -self.ScheduleDelayTime=DelayTime -self.ScheduleRepeatInterval=RepeatInterval -self.DetectionScheduler=SCHEDULER:New(self,self._DetectionScheduler,{self,"Detection"},DelayTime,RepeatInterval) -return self -end -end -do -DETECTION_UNITS={ -ClassName="DETECTION_UNITS", -DetectionRange=nil, -} -function DETECTION_UNITS:New(DetectionSetGroup) -local self=BASE:Inherit(self,DETECTION_BASE:New(DetectionSetGroup)) -self._SmokeDetectedUnits=false -self._FlareDetectedUnits=false -self._SmokeDetectedZones=false -self._FlareDetectedZones=false -self._BoundDetectedZones=false -return self -end -function DETECTION_UNITS:GetChangeText(DetectedItem) -self:F(DetectedItem) -local MT={} -for ChangeCode,ChangeData in pairs(DetectedItem.Changes)do -if ChangeCode=="AU"then -local MTUT={} -for ChangeUnitType,ChangeUnitCount in pairs(ChangeData)do -if ChangeUnitType~="ID"then -MTUT[#MTUT+1]=ChangeUnitCount.." of "..ChangeUnitType -end -end -MT[#MT+1]=" New target(s) detected: "..table.concat(MTUT,", ").."." -end -if ChangeCode=="RU"then -local MTUT={} -for ChangeUnitType,ChangeUnitCount in pairs(ChangeData)do -if ChangeUnitType~="ID"then -MTUT[#MTUT+1]=ChangeUnitCount.." of "..ChangeUnitType -end -end -MT[#MT+1]=" Invisible or destroyed target(s): "..table.concat(MTUT,", ").."." -end -end -return table.concat(MT,"\n") -end -function DETECTION_UNITS:CreateDetectionItems() -self:F2(#self.DetectedObjects) -for DetectedItemID,DetectedItem in pairs(self.DetectedItems)do -local DetectedItemSet=DetectedItem.Set -for DetectedUnitName,DetectedUnitData in pairs(DetectedItemSet:GetSet())do -local DetectedUnit=DetectedUnitData -local DetectedObject=nil -if DetectedUnit:IsAlive()then -DetectedObject=self:GetDetectedObject(DetectedUnit:GetName()) -end -if DetectedObject then -self:IdentifyDetectedObject(DetectedObject) -DetectedItem.TypeName=DetectedUnit:GetTypeName() -DetectedItem.CategoryName=DetectedUnit:GetCategoryName() -DetectedItem.Name=DetectedObject.Name -DetectedItem.IsVisible=DetectedObject.IsVisible -DetectedItem.LastTime=DetectedObject.LastTime -DetectedItem.LastPos=DetectedObject.LastPos -DetectedItem.LastVelocity=DetectedObject.LastVelocity -DetectedItem.KnowType=DetectedObject.KnowType -DetectedItem.KnowDistance=DetectedObject.KnowDistance -DetectedItem.Distance=DetectedObject.Distance -else -self:AddChangeUnit(DetectedItem,"RU",DetectedUnitName) -DetectedItemSet:Remove(DetectedUnitName) -end -end -end -for DetectedUnitName,DetectedObjectData in pairs(self.DetectedObjects)do -local DetectedObject=self:GetDetectedObject(DetectedUnitName) -if DetectedObject then -self:T({"Detected Unit #",DetectedUnitName}) -local DetectedUnit=UNIT:FindByName(DetectedUnitName) -if DetectedUnit then -local DetectedTypeName=DetectedUnit:GetTypeName() -local DetectedItem=self:GetDetectedItem(DetectedUnitName) -if not DetectedItem then -self:T("Added new DetectedItem") -DetectedItem=self:AddDetectedItem("UNIT",DetectedUnitName) -DetectedItem.TypeName=DetectedUnit:GetTypeName() -DetectedItem.Name=DetectedObject.Name -DetectedItem.IsVisible=DetectedObject.IsVisible -DetectedItem.LastTime=DetectedObject.LastTime -DetectedItem.LastPos=DetectedObject.LastPos -DetectedItem.LastVelocity=DetectedObject.LastVelocity -DetectedItem.KnowType=DetectedObject.KnowType -DetectedItem.KnowDistance=DetectedObject.KnowDistance -DetectedItem.Distance=DetectedObject.Distance -end -DetectedItem.Set:AddUnit(DetectedUnit) -self:AddChangeUnit(DetectedItem,"AU",DetectedTypeName) -end -end -end -for DetectedItemID,DetectedItemData in pairs(self.DetectedItems)do -local DetectedItem=DetectedItemData -local DetectedSet=DetectedItem.Set -local DetectedFirstUnit=DetectedSet:GetFirst() -local DetectedFirstUnitCoord=DetectedFirstUnit:GetCoordinate() -self:SetDetectedItemCoordinate(DetectedItem,DetectedFirstUnitCoord,DetectedFirstUnit) -self:ReportFriendliesNearBy({DetectedItem=DetectedItem,ReportSetGroup=self.DetectionSetGroup}) -end -end -function DETECTION_UNITS:DetectedItemMenu(Index,AttackGroup) -self:F(Index) -local DetectedItem=self:GetDetectedItem(Index) -local DetectedSet=self:GetDetectedSet(Index) -local DetectedItemID=self:GetDetectedItemID(Index) -self:T(DetectedSet) -if DetectedSet then -local ReportSummary="" -local UnitDistanceText="" -local UnitCategoryText="" -local DetectedItemCoordinate=self:GetDetectedItemCoordinate(Index) -local DetectedItemCoordText=DetectedItemCoordinate:ToString(AttackGroup) -ReportSummary=string.format( -"%s - %s", -DetectedItemID, -DetectedItemCoordText -) -self:T(ReportSummary) -return ReportSummary -end -end -function DETECTION_UNITS:DetectedItemReportSummary(Index,AttackGroup,Settings) -self:F({Index,self.DetectedItems}) -local DetectedItem=self:GetDetectedItem(Index) -local DetectedItemID=self:GetDetectedItemID(Index) -if DetectedItem then -local ReportSummary="" -local UnitDistanceText="" -local UnitCategoryText="" -if DetectedItem.KnowType then -local UnitCategoryName=DetectedItem.CategoryName -if UnitCategoryName then -UnitCategoryText=UnitCategoryName -end -if DetectedItem.TypeName then -UnitCategoryText=UnitCategoryText.." ("..DetectedItem.TypeName..")" -end -else -UnitCategoryText="Unknown" -end -if DetectedItem.KnowDistance then -if DetectedItem.IsVisible then -UnitDistanceText=" at "..string.format("%.2f",DetectedItem.Distance).." km" -end -else -if DetectedItem.IsVisible then -UnitDistanceText=" at +/- "..string.format("%.0f",DetectedItem.Distance).." km" -end -end -local DetectedItemCoordinate=self:GetDetectedItemCoordinate(Index) -local DetectedItemCoordText=DetectedItemCoordinate:ToString(AttackGroup,Settings) -local ThreatLevelA2G=self:GetDetectedItemThreatLevel(Index) -local Report=REPORT:New() -Report:Add(DetectedItemID..", "..DetectedItemCoordText) -Report:Add(string.format("Threat: [%s]",string.rep("■",ThreatLevelA2G))) -Report:Add(string.format("Type: %s%s",UnitCategoryText,UnitDistanceText)) -return Report -end -return nil -end -function DETECTION_UNITS:DetectedReportDetailed(AttackGroup) -self:F() -local Report=REPORT:New() -for DetectedItemID,DetectedItem in pairs(self.DetectedItems)do -local DetectedItem=DetectedItem -local ReportSummary=self:DetectedItemReportSummary(DetectedItemID,AttackGroup) -Report:SetTitle("Detected units:") -Report:Add(ReportSummary:Text()) -end -local ReportText=Report:Text() -return ReportText -end -end -do -DETECTION_TYPES={ -ClassName="DETECTION_TYPES", -DetectionRange=nil, -} -function DETECTION_TYPES:New(DetectionSetGroup) -local self=BASE:Inherit(self,DETECTION_BASE:New(DetectionSetGroup)) -self._SmokeDetectedUnits=false -self._FlareDetectedUnits=false -self._SmokeDetectedZones=false -self._FlareDetectedZones=false -self._BoundDetectedZones=false -return self -end -function DETECTION_TYPES:GetChangeText(DetectedItem) -self:F(DetectedItem) -local MT={} -for ChangeCode,ChangeData in pairs(DetectedItem.Changes)do -if ChangeCode=="AU"then -local MTUT={} -for ChangeUnitType,ChangeUnitCount in pairs(ChangeData)do -if ChangeUnitType~="ID"then -MTUT[#MTUT+1]=ChangeUnitCount.." of "..ChangeUnitType -end -end -MT[#MT+1]=" New target(s) detected: "..table.concat(MTUT,", ").."." -end -if ChangeCode=="RU"then -local MTUT={} -for ChangeUnitType,ChangeUnitCount in pairs(ChangeData)do -if ChangeUnitType~="ID"then -MTUT[#MTUT+1]=ChangeUnitCount.." of "..ChangeUnitType -end -end -MT[#MT+1]=" Invisible or destroyed target(s): "..table.concat(MTUT,", ").."." -end -end -return table.concat(MT,"\n") -end -function DETECTION_TYPES:CreateDetectionItems() -self:F2(#self.DetectedObjects) -for DetectedItemID,DetectedItem in pairs(self.DetectedItems)do -local DetectedItemSet=DetectedItem.Set -local DetectedTypeName=DetectedItem.TypeName -for DetectedUnitName,DetectedUnitData in pairs(DetectedItemSet:GetSet())do -local DetectedUnit=DetectedUnitData -local DetectedObject=nil -if DetectedUnit:IsAlive()then -DetectedObject=self:GetDetectedObject(DetectedUnit:GetName()) -end -if DetectedObject then -self:IdentifyDetectedObject(DetectedObject) -else -self:AddChangeUnit(DetectedItem,"RU",DetectedUnitName) -DetectedItemSet:Remove(DetectedUnitName) -end -end -end -for DetectedUnitName,DetectedObjectData in pairs(self.DetectedObjects)do -local DetectedObject=self:GetDetectedObject(DetectedUnitName) -if DetectedObject then -self:T({"Detected Unit #",DetectedUnitName}) -local DetectedUnit=UNIT:FindByName(DetectedUnitName) -if DetectedUnit then -local DetectedTypeName=DetectedUnit:GetTypeName() -local DetectedItem=self:GetDetectedItem(DetectedTypeName) -if not DetectedItem then -DetectedItem=self:AddDetectedItem("TYPE",DetectedTypeName) -DetectedItem.TypeName=DetectedUnit:GetTypeName() -end -DetectedItem.Set:AddUnit(DetectedUnit) -self:AddChangeUnit(DetectedItem,"AU",DetectedTypeName) -end -end -end -for DetectedItemID,DetectedItemData in pairs(self.DetectedItems)do -local DetectedItem=DetectedItemData -local DetectedSet=DetectedItem.Set -local DetectedFirstUnit=DetectedSet:GetFirst() -local DetectedUnitCoord=DetectedFirstUnit:GetCoordinate() -self:SetDetectedItemCoordinate(DetectedItem,DetectedUnitCoord,DetectedFirstUnit) -self:ReportFriendliesNearBy({DetectedItem=DetectedItem,ReportSetGroup=self.DetectionSetGroup}) -end -end -function DETECTION_TYPES:DetectedItemMenu(DetectedTypeName,AttackGroup) -self:F(DetectedTypeName) -local DetectedItem=self:GetDetectedItem(DetectedTypeName) -local DetectedItemID=self:GetDetectedItemID(DetectedTypeName) -if DetectedItem then -local DetectedItemCoordinate=self:GetDetectedItemCoordinate(DetectedTypeName) -local DetectedItemCoordText=DetectedItemCoordinate:ToString(AttackGroup) -local ReportSummary=string.format( -"%s - %s", -DetectedItemID, -DetectedItemCoordText -) -self:T(ReportSummary) -return ReportSummary -end -end -function DETECTION_TYPES:DetectedItemReportSummary(DetectedTypeName,AttackGroup,Settings) -self:F(DetectedTypeName) -local DetectedItem=self:GetDetectedItem(DetectedTypeName) -local DetectedSet=self:GetDetectedSet(DetectedTypeName) -local DetectedItemID=self:GetDetectedItemID(DetectedTypeName) -self:T(DetectedItem) -if DetectedItem then -local ThreatLevelA2G=self:GetDetectedItemThreatLevel(DetectedTypeName) -local DetectedItemsCount=DetectedSet:Count() -local DetectedItemType=DetectedItem.TypeName -local DetectedItemCoordinate=self:GetDetectedItemCoordinate(DetectedTypeName) -local DetectedItemCoordText=DetectedItemCoordinate:ToString(AttackGroup,Settings) -local Report=REPORT:New() -Report:Add(DetectedItemID..", "..DetectedItemCoordText) -Report:Add(string.format("Threat: [%s]",string.rep("■",ThreatLevelA2G))) -Report:Add(string.format("Type: %2d of %s",DetectedItemsCount,DetectedItemType)) -return Report -end -end -function DETECTION_TYPES:DetectedReportDetailed(AttackGroup) -self:F() -local Report=REPORT:New() -for DetectedItemTypeName,DetectedItem in pairs(self.DetectedItems)do -local DetectedItem=DetectedItem -local ReportSummary=self:DetectedItemReportSummary(DetectedItemTypeName,AttackGroup) -Report:SetTitle("Detected types:") -Report:Add(ReportSummary:Text()) -end -local ReportText=Report:Text() -return ReportText -end -end -do -DETECTION_AREAS={ -ClassName="DETECTION_AREAS", -DetectionZoneRange=nil, -} -function DETECTION_AREAS:New(DetectionSetGroup,DetectionZoneRange) -local self=BASE:Inherit(self,DETECTION_BASE:New(DetectionSetGroup)) -self.DetectionZoneRange=DetectionZoneRange -self._SmokeDetectedUnits=false -self._FlareDetectedUnits=false -self._SmokeDetectedZones=false -self._FlareDetectedZones=false -self._BoundDetectedZones=false -return self -end -function DETECTION_AREAS:DetectedItemMenu(Index,AttackGroup) -self:F(Index) -local DetectedItem=self:GetDetectedItem(Index) -local DetectedItemID=self:GetDetectedItemID(Index) -if DetectedItem then -local DetectedSet=self:GetDetectedSet(Index) -local ReportSummaryItem -local DetectedZone=self:GetDetectedItemZone(Index) -local DetectedItemCoordinate=DetectedZone:GetCoordinate() -local DetectedItemCoordText=DetectedItemCoordinate:ToString(AttackGroup) -local ReportSummary=string.format( -"%s - %s", -DetectedItemID, -DetectedItemCoordText -) -return ReportSummary -end -return nil -end -function DETECTION_AREAS:DetectedItemReportSummary(Index,AttackGroup,Settings) -self:F(Index) -local DetectedItem=self:GetDetectedItem(Index) -local DetectedItemID=self:GetDetectedItemID(Index) -if DetectedItem then -local DetectedSet=self:GetDetectedSet(Index) -local ReportSummaryItem -local DetectedZone=self:GetDetectedItemZone(Index) -local DetectedItemCoordinate=DetectedZone:GetCoordinate() -local DetectedItemCoordText=DetectedItemCoordinate:ToString(AttackGroup,Settings) -local ThreatLevelA2G=self:GetDetectedItemThreatLevel(Index) -local DetectedItemsCount=DetectedSet:Count() -local DetectedItemsTypes=DetectedSet:GetTypeNames() -local Report=REPORT:New() -Report:Add(DetectedItemID..", "..DetectedItemCoordText) -Report:Add(string.format("Threat: [%s]",string.rep("■",ThreatLevelA2G))) -Report:Add(string.format("Type: %2d of %s",DetectedItemsCount,DetectedItemsTypes)) -return Report -end -return nil -end -function DETECTION_AREAS:DetectedReportDetailed(AttackGroup) -self:F() -local Report=REPORT:New() -for DetectedItemIndex,DetectedItem in pairs(self.DetectedItems)do -local DetectedItem=DetectedItem -local ReportSummary=self:DetectedItemReportSummary(DetectedItemIndex,AttackGroup) -Report:SetTitle("Detected areas:") -Report:Add(ReportSummary:Text()) -end -local ReportText=Report:Text() -return ReportText -end -function DETECTION_AREAS:CalculateIntercept(DetectedItem) -local DetectedCoord=DetectedItem.Coordinate -local DetectedSpeed=DetectedCoord:GetVelocity() -local DetectedHeading=DetectedCoord:GetHeading() -if self.Intercept then -local DetectedSet=DetectedItem.Set -local TranslateDistance=DetectedSpeed*self.InterceptDelay -local InterceptCoord=DetectedCoord:Translate(TranslateDistance,DetectedHeading) -DetectedItem.InterceptCoord=InterceptCoord -else -DetectedItem.InterceptCoord=DetectedCoord -end -end -function DETECTION_AREAS:NearestFAC(DetectedItem) -local NearestRecce=nil -local DistanceRecce=1000000000 -for RecceGroupName,RecceGroup in pairs(self.DetectionSetGroup:GetSet())do -if RecceGroup and RecceGroup:IsAlive()then -for RecceUnit,RecceUnit in pairs(RecceGroup:GetUnits())do -if RecceUnit:IsActive()then -local RecceUnitCoord=RecceUnit:GetCoordinate() -local Distance=RecceUnitCoord:Get2DDistance(self:GetDetectedItemCoordinate(DetectedItem.Index)) -if Distance=timer.getTime()))then -TargetSetUnit:ForEachUnitPerThreatLevel(10,0, -function(TargetUnit) -self:F({TargetUnit=TargetUnit:GetName()}) -if MarkingCountFLmax then -FLmin=FLmax*0.75 -end -if self.category==RAT.cat.heli then -FLmin=math.max(H_departure,H_destination)+50 -FLmax=math.max(H_departure,H_destination)+1000 -end -FLmax=math.min(FLmax,self.aircraft.ceiling*0.9) -if self.FLminuser then -FLmin=self.FLminuser -end -if self.FLmaxuser then -FLmax=self.FLmaxuser -end -if self.aircraft.FLcruiseFLmax then -self.aircraft.FLcruise=FLmax -end -local FLcruise=self:_Random_Gaussian(self.aircraft.FLcruise,(FLmax-FLmin)/4,FLmin,FLmax) -if self.FLuser then -FLcruise=self.FLuser -end -local h_climb=FLcruise-H_departure -local d_climb=h_climb/math.tan(PhiClimb) -local h_descent=FLcruise-(H_holding+h_holding) -local d_descent=h_descent/math.tan(PhiDescent) -local d_cruise=d_total-d_climb-d_descent -local text=string.format("\n******************************************************\n") -text=text..string.format("Template = %s\n\n",self.SpawnTemplatePrefix) -text=text..string.format("Speeds:\n") -text=text..string.format("VxCruiseMin = %6.1f m/s = %5.1f km/h\n",VxCruiseMin,VxCruiseMin*3.6) -text=text..string.format("VxCruiseMax = %6.1f m/s = %5.1f km/h\n",VxCruiseMax,VxCruiseMax*3.6) -text=text..string.format("VxCruise = %6.1f m/s = %5.1f km/h\n",VxCruise,VxCruise*3.6) -text=text..string.format("VxClimb = %6.1f m/s = %5.1f km/h\n",VxClimb,VxClimb*3.6) -text=text..string.format("VxDescent = %6.1f m/s = %5.1f km/h\n",VxDescent,VxDescent*3.6) -text=text..string.format("VxHolding = %6.1f m/s = %5.1f km/h\n",VxHolding,VxHolding*3.6) -text=text..string.format("VxFinal = %6.1f m/s = %5.1f km/h\n",VxFinal,VxFinal*3.6) -text=text..string.format("VyClimb = %6.1f m/s\n",VyClimb) -text=text..string.format("\nDistances:\n") -text=text..string.format("d_climb = %6.1f km\n",d_climb/1000) -text=text..string.format("d_cruise = %6.1f km\n",d_cruise/1000) -text=text..string.format("d_descent = %6.1f km\n",d_descent/1000) -text=text..string.format("d_holding = %6.1f km\n",d_holding/1000) -text=text..string.format("d_total = %6.1f km\n",d_total/1000) -text=text..string.format("\nHeights:\n") -text=text..string.format("H_departure = %6.1f m ASL\n",H_departure) -text=text..string.format("H_destination = %6.1f m ASL\n",H_destination) -text=text..string.format("H_holding = %6.1f m ASL\n",H_holding) -text=text..string.format("h_climb = %6.1f m\n",h_climb) -text=text..string.format("h_descent = %6.1f m\n",h_descent) -text=text..string.format("h_holding = %6.1f m\n",h_holding) -text=text..string.format("delta H = %6.1f m\n",deltaH) -text=text..string.format("FLmin = %6.1f m ASL = FL%03d\n",FLmin,FLmin/RAT.unit.FL2m) -text=text..string.format("FLcruise = %6.1f m ASL = FL%03d\n",FLcruise,FLcruise/RAT.unit.FL2m) -text=text..string.format("FLmax = %6.1f m ASL = FL%03d\n",FLmax,FLmax/RAT.unit.FL2m) -text=text..string.format("\nAngles:\n") -text=text..string.format("Alpha climb = %6.1f Deg\n",math.deg(AlphaClimb)) -text=text..string.format("Alpha descent = %6.1f Deg\n",math.deg(AlphaDescent)) -text=text..string.format("Phi (slope) = %6.1f Deg\n",math.deg(phi)) -text=text..string.format("Phi climb = %6.1f Deg\n",math.deg(PhiClimb)) -text=text..string.format("Phi descent = %6.1f Deg\n",math.deg(PhiDescent)) -text=text..string.format("Heading = %6.1f Deg\n",heading) -text=text..string.format("******************************************************\n") -env.info(RAT.id..text) -if d_cruise<0 then -d_cruise=100 -end -local c0=Pdeparture -local c1=c0:Translate(d_climb/2,heading) -local c2=c1:Translate(d_climb/2,heading) -local c3=c2:Translate(d_cruise,heading) -local c4=c3:Translate(d_descent/2,heading) -local c5=Pholding -local c6=Pdestination -local wp0=self:_Waypoint(takeoff,c0,VxClimb,H_departure,departure) -local wp1=self:_Waypoint(RAT.wp.climb,c1,VxClimb,H_departure+(FLcruise-H_departure)/2) -local wp2=self:_Waypoint(RAT.wp.cruise,c2,VxCruise,FLcruise) -local wp3=self:_Waypoint(RAT.wp.cruise,c3,VxCruise,FLcruise) -local wp4=self:_Waypoint(RAT.wp.descent,c4,VxDescent,FLcruise-(FLcruise-(h_holding+H_holding))/2) -local wp5=self:_Waypoint(RAT.wp.holding,c5,VxHolding,H_holding+h_holding) -local wp6=self:_Waypoint(RAT.wp.landing,c6,VxFinal,H_destination,destination) -local waypoints={wp0,wp1,wp2,wp3,wp4,wp5,wp6} -if self.placemarkers then -self:_PlaceMarkers(waypoints) -end -self:_Routeinfo(waypoints,"Waypoint info in set_route:") -return departure,destination,waypoints -end -function RAT:_PickDeparture(takeoff) -local departures={} -if takeoff==RAT.wp.air then -if self.random_departure then -for _,airport in pairs(self.airports)do -if not self:_Excluded(airport:GetName())then -table.insert(departures,airport:GetZone()) -end -end -else -for _,name in pairs(self.departure_zones)do -if not self:_Excluded(name)then -table.insert(departures,ZONE:New(name)) -end -end -for _,name in pairs(self.departure_ports)do -if not self:_Excluded(name)then -table.insert(departures,AIRBASE:FindByName(name):GetZone()) -end -end -end -else -if self.random_departure then -for _,airport in pairs(self.airports)do -if not self:_Excluded(airport:GetName())then -table.insert(departures,airport) -end -end -else -for _,name in pairs(self.departure_ports)do -if not self:_Excluded(name)then -table.insert(departures,AIRBASE:FindByName(name)) -end -end -end -end -local departure=departures[math.random(#departures)] -local text -if departure and departure:GetName()then -if takeoff==RAT.wp.air then -text="Chosen departure zone: "..departure:GetName() -else -text="Chosen departure airport: "..departure:GetName().." (ID "..departure:GetID()..")" -end -env.info(RAT.id..text) -if self.debug then -MESSAGE:New(text,30):ToAll() -end -else -departure=nil -end -return departure -end -function RAT:_PickDestination(destinations,_random) -local destination=nil -if destinations and#destinations>0 then -destination=destinations[math.random(#destinations)] -local text="Chosen destination airport: "..destination:GetName().." (ID "..destination:GetID()..")" -env.info(RAT.id..text) -if self.debug then -MESSAGE:New(text,30):ToAll() -end -else -env.error(RAT.id.."No destination airport found.") -end -return destination -end -function RAT:_GetDestinations(departure,q,minrange,maxrange) -minrange=minrange or self.mindist -maxrange=maxrange or self.maxdist -local possible_destinations={} -if self.random_destination then -for _,airport in pairs(self.airports)do -local name=airport:GetName() -if self:_IsFriendly(name)and not self:_Excluded(name)and name~=departure:GetName()then -local distance=q:Get2DDistance(airport:GetCoordinate()) -if distance>=minrange and distance<=maxrange then -table.insert(possible_destinations,airport) -end -end -end -else -for _,name in pairs(self.destination_ports)do -if name~=departure:GetName()then -local airport=AIRBASE:FindByName(name) -table.insert(possible_destinations,airport) -end -end -end -env.info(RAT.id.."Number of possible destination airports = "..#possible_destinations) -if#possible_destinations>0 then -local function compare(a,b) -local qa=q:Get2DDistance(a:GetCoordinate()) -local qb=q:Get2DDistance(b:GetCoordinate()) -return qaself.Tinactive then -if Dg<50 then -stationary=true -end -self.ratcraft[i]["Tlastcheck"]=Tnow -self.ratcraft[i]["Pground"]=coords -end -else -self.ratcraft[i]["Tground"]=Tnow -self.ratcraft[i]["Tlastcheck"]=Tnow -self.ratcraft[i]["Pground"]=coords -end -end -local Pn=coords -local Dtravel=Pn:Get2DDistance(self.ratcraft[i]["Pnow"]) -self.ratcraft[i]["Pnow"]=Pn -self.ratcraft[i]["Distance"]=self.ratcraft[i]["Distance"]+Dtravel -local Ddestination=Pn:Get2DDistance(self.ratcraft[i].destination:GetCoordinate()) -local Hp=COORDINATE:New(self.ratcraft[i].waypoints[6].x,self.ratcraft[i].waypoints[6].alt,self.ratcraft[i].waypoints[6].y) -local Dholding=Pn:Get2DDistance(Hp) -local status=self.ratcraft[i].status -local DRholding -if self.category==RAT.cat.plane then -DRholding=8000 -else -DRholding=2000 -end -if self.ATCswitch and Dholding<=DRholding and string.match(status,"On journey")then -RAT:_ATCRegisterFlight(group:GetName(),Tnow) -self.ratcraft[i].status="Holding" -end -if(forID and i==forID)or(not forID)then -local text=string.format("ID %i of group %s\n",i,prefix) -if self.commute then -text=text..string.format("%s commuting between %s and %s\n",type,departure,destination) -elseif self.continuejourney then -text=text..string.format("%s travelling from %s to %s (and continueing form there)\n",type,departure,destination) -else -text=text..string.format("%s travelling from %s to %s\n",type,departure,destination) -end -text=text..string.format("Status: %s",self.ratcraft[i].status) -if airborne then -text=text.." [airborne]\n" -else -text=text.." [on ground]\n" -end -text=text..string.format("Fuel = %3.0f %%\n",fuel) -text=text..string.format("Life = %3.0f %%\n",life) -text=text..string.format("FL%03d = %i m\n",alt/RAT.unit.FL2m,alt) -text=text..string.format("Distance travelled = %6.1f km\n",self.ratcraft[i]["Distance"]/1000) -text=text..string.format("Distance to destination = %6.1f km",Dholding/1000) -if not airborne then -text=text..string.format("\nTime on ground = %6.0f seconds\n",Tg) -text=text..string.format("Position change = %8.1f m since %3.0f seconds.",Dg,dTlast) -end -if self.debug then -env.info(RAT.id..text) -end -if self.reportstatus or message then -MESSAGE:New(text,20):ToAll() -end -end -if not airborne then -if stationary then -local text=string.format("Group %s is despawned after being %4.0f seconds inaktive on ground.",self.SpawnTemplatePrefix,dTlast) -env.info(RAT.id..text) -self:_Despawn(group) -end -if life<10 and Dtravel<100 then -local text=string.format("Damaged group %s is despawned. Life = %3.0f",self.SpawnTemplatePrefix,life) -self:_Despawn(group) -end -end -end -else -if self.debug then -local text=string.format("Group %i does not exist.",i) -env.info(RAT.id..text) -end -end -end -end -function RAT:_GetLife(group) -local life=0.0 -if group and group:IsAlive()then -local unit=group:GetUnit(1) -if unit then -life=unit:GetLife()/unit:GetLife0()*100 -else -if self.debug then -env.error(RAT.id.."Unit does not exist in RAT_Getlife(). Returning zero.") -end -end -else -if self.debug then -env.error(RAT.id.."Group does not exist in RAT_Getlife(). Returning zero.") -end -end -return life -end -function RAT:_SetStatus(group,status) -local index=self:GetSpawnIndexFromGroup(group) -env.info(RAT.id.."Status for group "..group:GetName()..": "..status) -self.ratcraft[index].status=status -end -function RAT:_OnBirth(EventData) -local SpawnGroup=EventData.IniGroup -if SpawnGroup then -local EventPrefix=self:_GetPrefixFromGroup(SpawnGroup) -if EventPrefix then -if EventPrefix==self.alias then -local text="Event: Group "..SpawnGroup:GetName().." was born." -env.info(RAT.id..text) -local status -if SpawnGroup:InAir()then -status="Just born (after air start)" -else -status="Starting engines (after birth)" -end -self:_SetStatus(SpawnGroup,status) -end -end -else -if self.debug then -env.error("Group does not exist in RAT:_OnBirthDay().") -end -end -end -function RAT:_EngineStartup(EventData) -local SpawnGroup=EventData.IniGroup -if SpawnGroup then -local EventPrefix=self:_GetPrefixFromGroup(SpawnGroup) -if EventPrefix then -if EventPrefix==self.alias then -local text="Event: Group "..SpawnGroup:GetName().." started engines." -env.info(RAT.id..text) -local status -if SpawnGroup:InAir()then -status="On journey (after air start)" -else -status="Taxiing (after engines started)" -end -self:_SetStatus(SpawnGroup,status) -end -end -else -if self.debug then -env.error("Group does not exist in RAT:_EngineStartup().") -end -end -end -function RAT:_OnTakeoff(EventData) -local SpawnGroup=EventData.IniGroup -if SpawnGroup then -local EventPrefix=self:_GetPrefixFromGroup(SpawnGroup) -if EventPrefix then -if EventPrefix==self.alias then -local text="Event: Group "..SpawnGroup:GetName().." is airborne." -env.info(RAT.id..text) -self:_SetStatus(SpawnGroup,"On journey (after takeoff)") -end -end -else -if self.debug then -env.error("Group does not exist in RAT:_OnTakeoff().") -end -end -end -function RAT:_OnLand(EventData) -local SpawnGroup=EventData.IniGroup -if SpawnGroup then -local EventPrefix=self:_GetPrefixFromGroup(SpawnGroup) -if EventPrefix then -if EventPrefix==self.alias then -local text="Event: Group "..SpawnGroup:GetName().." landed." -env.info(RAT.id..text) -self:_SetStatus(SpawnGroup,"Taxiing (after landing)") -if self.ATCswitch then -RAT:_ATCFlightLanded(SpawnGroup:GetName()) -end -if self.respawn_at_landing then -text="Event: Group "..SpawnGroup:GetName().." will be respawned." -env.info(RAT.id..text) -self:_Respawn(SpawnGroup) -end -end -end -else -if self.debug then -env.error("Group does not exist in RAT:_OnLand().") -end -end -end -function RAT:_OnEngineShutdown(EventData) -local SpawnGroup=EventData.IniGroup -if SpawnGroup then -local EventPrefix=self:_GetPrefixFromGroup(SpawnGroup) -if EventPrefix then -if EventPrefix==self.alias then -local text="Event: Group "..SpawnGroup:GetName().." shut down its engines." -env.info(RAT.id..text) -self:_SetStatus(SpawnGroup,"Parking (shutting down engines)") -if not self.respawn_at_landing then -text="Event: Group "..SpawnGroup:GetName().." will be respawned." -env.info(RAT.id..text) -self:_Respawn(SpawnGroup) -end -text="Event: Group "..SpawnGroup:GetName().." will be destroyed now." -env.info(RAT.id..text) -self:_Despawn(SpawnGroup) -end -end -else -if self.debug then -env.error("Group does not exist in RAT:_OnEngineShutdown().") -end -end -end -function RAT:_OnDead(EventData) -local SpawnGroup=EventData.IniGroup -if SpawnGroup then -local EventPrefix=self:_GetPrefixFromGroup(SpawnGroup) -if EventPrefix then -if EventPrefix==self.alias then -local text="Event: Group "..SpawnGroup:GetName().." died." -env.info(RAT.id..text) -self:_SetStatus(SpawnGroup,"Destroyed (after dead)") -end -end -else -if self.debug then -env.error("Group does not exist in RAT:_OnDead().") -end -end -end -function RAT:_OnCrash(EventData) -local SpawnGroup=EventData.IniGroup -env.info(string.format("%sGroup %s crashed!",RAT.id,SpawnGroup:GetName())) -if SpawnGroup then -local EventPrefix=self:_GetPrefixFromGroup(SpawnGroup) -if EventPrefix then -if EventPrefix==self.alias then -local text="Event: Group "..SpawnGroup:GetName().." crashed." -env.info(RAT.id..text) -self:_SetStatus(SpawnGroup,"Crashed") -end -end -else -if self.debug then -env.error("Group does not exist in RAT:_OnCrash().") -end -end -end -function RAT:_Despawn(group) -local index=self:GetSpawnIndexFromGroup(group) -self.ratcraft[index].group:Destroy() -self.ratcraft[index].group=nil -self.alive=self.alive-1 -if self.f10menu then -self.Menu[self.SubMenuName]["groups"][index]:Remove() -end -end -function RAT:_Waypoint(Type,Coord,Speed,Altitude,Airport) -local _Altitude=Altitude or Coord.y -local Hland=Coord:GetLandHeight() -local _Type=nil -local _Action=nil -local _alttype="RADIO" -local _AID=nil -if Type==RAT.wp.cold then -_Type="TakeOffParking" -_Action="From Parking Area" -_Altitude=0 -_alttype="RADIO" -_AID=Airport:GetID() -elseif Type==RAT.wp.hot then -_Type="TakeOffParkingHot" -_Action="From Parking Area Hot" -_Altitude=0 -_alttype="RADIO" -_AID=Airport:GetID() -elseif Type==RAT.wp.runway then -_Type="TakeOff" -_Action="From Parking Area" -_Altitude=0 -_alttype="RADIO" -_AID=Airport:GetID() -elseif Type==RAT.wp.air then -_Type="Turning Point" -_Action="Turning Point" -_alttype="BARO" -elseif Type==RAT.wp.climb then -_Type="Turning Point" -_Action="Turning Point" -_alttype="BARO" -elseif Type==RAT.wp.cruise then -_Type="Turning Point" -_Action="Turning Point" -_alttype="BARO" -elseif Type==RAT.wp.descent then -_Type="Turning Point" -_Action="Turning Point" -_alttype="BARO" -elseif Type==RAT.wp.holding then -_Type="Turning Point" -_Action="Turning Point" -_alttype="BARO" -elseif Type==RAT.wp.landing then -_Type="Land" -_Action="Landing" -_Altitude=0 -_alttype="RADIO" -_AID=Airport:GetID() -else -env.error("Unknown waypoint type in RAT:Waypoint() function!") -_Type="Turning Point" -_Action="Turning Point" -_alttype="RADIO" -end -local text=string.format("\n******************************************************\n") -text=text..string.format("Template = %s\n",self.SpawnTemplatePrefix) -text=text..string.format("Type: %i - %s\n",Type,_Type) -text=text..string.format("Action: %s\n",_Action) -text=text..string.format("Coord: x = %6.1f km, y = %6.1f km, alt = %6.1f m\n",Coord.x/1000,Coord.z/1000,Coord.y) -text=text..string.format("Speed = %6.1f m/s = %6.1f km/h = %6.1f knots\n",Speed,Speed*3.6,Speed*1.94384) -text=text..string.format("Land = %6.1f m ASL\n",Hland) -text=text..string.format("Altitude = %6.1f m (%s)\n",_Altitude,_alttype) -if Airport then -if Type==RAT.wp.air then -text=text..string.format("Zone = %s\n",Airport:GetName()) -else -text=text..string.format("Airport = %s with ID %i\n",Airport:GetName(),Airport:GetID()) -end -else -text=text..string.format("No airport/zone specified\n") -end -text=text.."******************************************************\n" -if self.debug then -env.info(RAT.id..text) -end -local RoutePoint={} -RoutePoint.x=Coord.x -RoutePoint.y=Coord.z -RoutePoint.alt=_Altitude -RoutePoint.alt_type=_alttype -RoutePoint.type=_Type -RoutePoint.action=_Action -RoutePoint.speed=Speed -RoutePoint.speed_locked=true -RoutePoint.ETA=nil -RoutePoint.ETA_locked=false -RoutePoint.name="RAT waypoint" -if(Airport~=nil)and Type~=RAT.wp.air then -local AirbaseID=Airport:GetID() -local AirbaseCategory=Airport:GetDesc().category -if AirbaseCategory==Airbase.Category.SHIP then -RoutePoint.linkUnit=AirbaseID -RoutePoint.helipadId=AirbaseID -elseif AirbaseCategory==Airbase.Category.HELIPAD then -RoutePoint.linkUnit=AirbaseID -RoutePoint.helipadId=AirbaseID -elseif AirbaseCategory==Airbase.Category.AIRDROME then -RoutePoint.airdromeId=AirbaseID -else -end -end -RoutePoint.properties={ -["vnav"]=1, -["scale"]=0, -["angle"]=0, -["vangle"]=0, -["steer"]=2, -} -if Type==RAT.wp.holding then -local Duration=self:_Randomize(90,0.9) -RoutePoint.task=self:_TaskHolding({x=Coord.x,y=Coord.z},Altitude,Speed,Duration) -else -RoutePoint.task={} -RoutePoint.task.id="ComboTask" -RoutePoint.task.params={} -RoutePoint.task.params.tasks={} -end -return RoutePoint -end -function RAT:_Routeinfo(waypoints,comment) -local text=string.format("\n******************************************************\n") -text=text..string.format("Template = %s\n",self.SpawnTemplatePrefix) -if comment then -text=text..comment.."\n" -end -text=text..string.format("Number of waypoints = %i\n",#waypoints) -for i=1,#waypoints do -local p=waypoints[i] -text=text..string.format("WP #%i: x = %6.1f km, y = %6.1f km, alt = %6.1f m\n",i-1,p.x/1000,p.y/1000,p.alt) -end -local total=0.0 -for i=1,#waypoints-1 do -local point1=waypoints[i] -local point2=waypoints[i+1] -local x1=point1.x -local y1=point1.y -local x2=point2.x -local y2=point2.y -local d=math.sqrt((x1-x2)^2+(y1-y2)^2) -local heading=self:_Course(point1,point2) -total=total+d -text=text..string.format("Distance from WP %i-->%i = %6.1f km. Heading = %i.\n",i-1,i,d/1000,heading) -end -text=text..string.format("Total distance = %6.1f km\n",total/1000) -local text=string.format("******************************************************\n") -if self.debug then -env.info(RAT.id..text) -end -return total -end -function RAT:_TaskHolding(P1,Altitude,Speed,Duration) -local dx=3000 -local dy=0 -if self.category==RAT.cat.heli then -dx=200 -dy=0 -end -local P2={} -P2.x=P1.x+dx -P2.y=P1.y+dy -local Task={ -id='Orbit', -params={ -pattern=AI.Task.OrbitPattern.RACE_TRACK, -point=P1, -point2=P2, -speed=Speed, -altitude=Altitude -} -} -local DCSTask={} -DCSTask.id="ControlledTask" -DCSTask.params={} -DCSTask.params.task=Task -if self.ATCswitch then -local userflagname=string.format("%s#%03d",self.alias,self.SpawnIndex+1) -DCSTask.params.stopCondition={userFlag=userflagname,userFlagValue=1,duration=1800} -else -DCSTask.params.stopCondition={duration=Duration} -end -return DCSTask -end -function RAT:_FLmax(alpha,beta,d,phi,h0) -local gamma=math.rad(180)-alpha-beta -local a=d*math.sin(alpha)/math.sin(gamma) -local b=d*math.sin(beta)/math.sin(gamma) -local h1=b*math.sin(alpha) -local h2=a*math.sin(beta) -local h3=b*math.cos(math.pi/2-(alpha+phi)) -local text=string.format("\nFLmax = FL%3.0f = %6.1f m.\n",h1/RAT.unit.FL2m,h1) -text=text..string.format("FLmax = FL%3.0f = %6.1f m.\n",h2/RAT.unit.FL2m,h2) -text=text..string.format("FLmax = FL%3.0f = %6.1f m.",h3/RAT.unit.FL2m,h3) -if self.debug then -env.info(RAT.id..text) -end -return h3+h0 -end -function RAT:_MinDistance(alpha,beta,h) -local d1=h/math.tan(alpha) -local d2=h/math.tan(beta) -return d1+d2 -end -function RAT:_AirportExists(name) -for _,airport in pairs(self.airports_map)do -if airport:GetName()==name then -return true -end -end -return false -end -function RAT:_SetROE(group,roe) -env.info(RAT.id.."Setting ROE to "..roe.." for group "..group:GetName()) -if self.roe==RAT.ROE.returnfire then -group:OptionROEReturnFire() -elseif self.roe==RAT.ROE.weaponfree then -group:OptionROEWeaponFree() -else -group:OptionROEHoldFire() -end -end -function RAT:_SetROT(group,rot) -env.info(RAT.id.."Setting ROT to "..rot.." for group "..group:GetName()) -if self.rot==RAT.ROT.passive then -group:OptionROTPassiveDefense() -elseif self.rot==RAT.ROT.evade then -group:OptionROTEvadeFire() -else -group:OptionROTNoReaction() -end -end -function RAT:_SetCoalitionTable() -if self.friendly==RAT.coal.neutral then -self.ctable={coalition.side.NEUTRAL} -elseif self.friendly==RAT.coal.same then -self.ctable={self.coalition,coalition.side.NEUTRAL} -elseif self.friendly==RAT.coal.sameonly then -self.ctable={self.coalition} -else -env.error("Unknown friendly coalition in _SetCoalitionTable(). Defaulting to NEUTRAL.") -self.ctable={self.coalition,coalition.side.NEUTRAL} -end -end -function RAT:_Course(a,b) -local dx=b.x-a.x -local ay -if a.alt then -ay=a.y -else -ay=a.z -end -local by -if b.alt then -by=b.y -else -by=b.z -end -local dy=by-ay -local angle=math.deg(math.atan2(dy,dx)) -if angle<0 then -angle=360+angle -end -return angle -end -function RAT:_Randomize(value,fac,lower,upper) -local min -if lower then -min=math.max(value-value*fac,lower) -else -min=value-value*fac -end -local max -if upper then -max=math.min(value+value*fac,upper) -else -max=value+value*fac -end -local r=math.random(min,max) -if self.debug then -local text=string.format("Random: value = %6.2f, fac = %4.2f, min = %6.2f, max = %6.2f, r = %6.2f",value,fac,min,max,r) -env.info(RAT.id..text) -end -return r -end -function RAT:_Random_Gaussian(x0,sigma,xmin,xmax) -sigma=sigma or 10 -local r -local gotit=false -local i=0 -while not gotit do -local x1=math.random() -local x2=math.random() -r=math.sqrt(-2*sigma*sigma*math.log(x1))*math.cos(2*math.pi*x2)+x0 -i=i+1 -if(r>=xmin and r<=xmax)or i>100 then -gotit=true -end -end -return r -end -function RAT:_PlaceMarkers(waypoints) -self:_SetMarker("Takeoff",waypoints[1]) -self:_SetMarker("Climb",waypoints[2]) -self:_SetMarker("Begin of Cruise",waypoints[3]) -self:_SetMarker("End of Cruise",waypoints[4]) -self:_SetMarker("Descent",waypoints[5]) -self:_SetMarker("Holding Point",waypoints[6]) -self:_SetMarker("Destination",waypoints[7]) -end -function RAT:_SetMarker(text,wp) -RAT.markerid=RAT.markerid+1 -self.markerids[#self.markerids+1]=RAT.markerid -if self.debug then -env.info(RAT.id..self.SpawnTemplatePrefix..": placing marker with ID "..RAT.markerid..": "..text) -end -local vec={x=wp.x,y=wp.alt,z=wp.y} -local text1=string.format("%s:\n%s",self.alias,text) -trigger.action.markToAll(RAT.markerid,text1,vec) -end -function RAT:_DeleteMarkers() -for k,v in ipairs(self.markerids)do -trigger.action.removeMark(v) -end -for k,v in ipairs(self.markerids)do -self.markerids[k]=nil -end -end -function RAT:_ModifySpawnTemplate(waypoints) -local PointVec3={x=waypoints[1].x,y=waypoints[1].alt,z=waypoints[1].y} -local heading=self:_Course(waypoints[1],waypoints[2]) -if self:_GetSpawnIndex(self.SpawnIndex+1)then -local SpawnTemplate=self.SpawnGroups[self.SpawnIndex].SpawnTemplate -if SpawnTemplate then -self:T(SpawnTemplate) -for UnitID=1,#SpawnTemplate.units do -self:T('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) -local UnitTemplate=SpawnTemplate.units[UnitID] -local SX=UnitTemplate.x -local SY=UnitTemplate.y -local BX=SpawnTemplate.route.points[1].x -local BY=SpawnTemplate.route.points[1].y -local TX=PointVec3.x+(SX-BX) -local TY=PointVec3.z+(SY-BY) -SpawnTemplate.units[UnitID].x=TX -SpawnTemplate.units[UnitID].y=TY -SpawnTemplate.units[UnitID].alt=PointVec3.y -SpawnTemplate.units[UnitID].heading=math.rad(heading) -if self.livery then -SpawnTemplate.units[UnitID].livery_id=self.livery[math.random(#self.livery)] -end -SpawnTemplate.units[UnitID]["skill"]=self.skill -SpawnTemplate.units[UnitID]["onboard_num"]=self.SpawnIndex -SpawnTemplate.CoalitionID=self.coalition -if self.country then -SpawnTemplate.CountryID=self.country -end -UnitTemplate.parking=nil -UnitTemplate.parking_id=nil -UnitTemplate.alt=PointVec3.y -self:T('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) -end -for i,wp in ipairs(waypoints)do -SpawnTemplate.route.points[i]=wp -end -SpawnTemplate.x=PointVec3.x -SpawnTemplate.y=PointVec3.z -self.SpawnGroups[self.SpawnIndex].SpawnTemplate=SpawnTemplate -self:T(SpawnTemplate) -end -end -end -function RAT:_ATCInit(airports_map) -if not RAT.ATC.init then -env.info(RAT.id.."Starting RAT ATC.") -RAT.ATC.init=true -for _,ap in pairs(airports_map)do -local name=ap:GetName() -RAT.ATC.airport[name]={} -RAT.ATC.airport[name].queue={} -RAT.ATC.airport[name].busy=false -RAT.ATC.airport[name].onfinal=nil -RAT.ATC.airport[name].traffic=0 -end -SCHEDULER:New(nil,RAT._ATCCheck,{self},5,15) -SCHEDULER:New(nil,RAT._ATCStatus,{self},5,60) -RAT.ATC.T0=timer.getTime() -end -end -function RAT:_ATCAddFlight(name,dest) -env.info(string.format("%s%s ATC: Adding flight %s with destination %s.",RAT.id,dest,name,dest)) -RAT.ATC.flight[name]={} -RAT.ATC.flight[name].destination=dest -RAT.ATC.flight[name].Tarrive=-1 -RAT.ATC.flight[name].holding=-1 -RAT.ATC.flight[name].Tonfinal=-1 -end -function RAT:_ATCDelFlight(t,entry) -for k,_ in pairs(t)do -if k==entry then -t[entry]=nil -end -end -end -function RAT:_ATCRegisterFlight(name,time) -RAT.ATC.flight[name].Tarrive=time -RAT.ATC.flight[name].holding=0 -end -function RAT:_ATCStatus() -local Tnow=timer.getTime() -for name,_ in pairs(RAT.ATC.flight)do -local hold=RAT.ATC.flight[name].holding -local dest=RAT.ATC.flight[name].destination -if hold>=0 then -local busy="Runway is currently clear" -if RAT.ATC.airport[dest].busy then -if RAT.ATC.airport[dest].onfinal then -busy="Runway is occupied by "..RAT.ATC.airport[dest].onfinal -else -busy="Runway is occupied" -end -end -env.info(string.format("%s%s ATC: Flight %s is holding for %i:%02d. %s.",RAT.id,dest,name,hold/60,hold%60,busy)) -elseif hold==RAT.ATC.onfinal then -local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal -env.info(string.format("%s%s ATC: Flight %s was cleared for landing. Waiting %i:%02d for landing event.",RAT.id,dest,name,Tfinal/60,Tfinal%60)) -if Tfinal>300 then -end -elseif hold==RAT.ATC.unregistered then -else -env.error(RAT.id.."Unknown holding time in RAT:_ATCStatus().") -end -end -end -function RAT:_ATCCheck() -RAT:_ATCQueue() -local Tnow=timer.getTime() -for name,_ in pairs(RAT.ATC.airport)do -local qw={} -for qID,flight in ipairs(RAT.ATC.airport[name].queue)do -local nqueue=#RAT.ATC.airport[name].queue -if RAT.ATC.airport[name].busy then -RAT.ATC.flight[flight].holding=Tnow-RAT.ATC.flight[flight].Tarrive -local text=string.format("%s ATC: Flight %s runway is busy. You are #%d of %d in landing queue. Your holding time is %i:%02d.",name,flight,qID,nqueue,RAT.ATC.flight[flight].holding/60,RAT.ATC.flight[flight].holding%60) -env.info(text) -else -RAT:_ATCClearForLanding(name,flight) -table.insert(qw,qID) -end -end -for _,i in pairs(qw)do -table.remove(RAT.ATC.airport[name].queue,i) -end -end -end -function RAT:_ATCClearForLanding(airport,flight) -RAT.ATC.flight[flight].holding=RAT.ATC.onfinal -RAT.ATC.airport[airport].busy=true -RAT.ATC.airport[airport].onfinal=flight -RAT.ATC.flight[flight].Tonfinal=timer.getTime() -trigger.action.setUserFlag(flight,1) -local flagvalue=trigger.misc.getUserFlag(flight) -local text1=string.format("%s%s ATC: Flight %s cleared for final approach (flag=%d).",RAT.id,airport,flight,flagvalue) -local text2=string.format("%s ATC: Flight %s you are cleared for landing.",airport,flight) -env.info(text1) -MESSAGE:New(text2,10):ToAll() -end -function RAT:_ATCFlightLanded(name) -if RAT.ATC.flight[name]then -local dest=RAT.ATC.flight[name].destination -local Tnow=timer.getTime() -local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal -local Thold=RAT.ATC.flight[name].Tonfinal-RAT.ATC.flight[name].Tarrive -RAT.ATC.airport[dest].busy=false -RAT.ATC.airport[dest].onfinal=nil -RAT:_ATCDelFlight(RAT.ATC.flight,name) -RAT.ATC.airport[dest].traffic=RAT.ATC.airport[dest].traffic+1 -local text1=string.format("%s%s ATC: Flight %s landed. Tholding = %i:%02d, Tfinal = %i:%02d.",RAT.id,dest,name,Thold/60,Thold%60,Tfinal/60,Tfinal%60) -local text2=string.format("%s ATC: Flight %s landed. Welcome to %s.",dest,name,dest) -env.info(text1) -env.info(string.format("%s%s ATC: Number of planes landed in total %d.",RAT.id,dest,RAT.ATC.airport[dest].traffic)) -MESSAGE:New(text2,10):ToAll() -end -end -function RAT:_ATCQueue() -for airport,_ in pairs(RAT.ATC.airport)do -local _queue={} -for name,_ in pairs(RAT.ATC.flight)do -local hold=RAT.ATC.flight[name].holding -local dest=RAT.ATC.flight[name].destination -if hold>=0 and airport==dest then -_queue[#_queue+1]={name,hold} -end -end -local function compare(a,b) -return a[2]>b[2] -end -table.sort(_queue,compare) -RAT.ATC.airport[airport].queue={} -for k,v in ipairs(_queue)do -table.insert(RAT.ATC.airport[airport].queue,v[1]) -end -end -end -AI_BALANCER={ -ClassName="AI_BALANCER", -PatrolZones={}, -AIGroups={}, -Earliest=5, -Latest=60, -} -function AI_BALANCER:New(SetClient,SpawnAI) -local self=BASE:Inherit(self,FSM_SET:New(SET_GROUP:New())) -self:SetStartState("None") -self:AddTransition("*","Monitor","Monitoring") -self:AddTransition("*","Spawn","Spawning") -self:AddTransition("Spawning","Spawned","Spawned") -self:AddTransition("*","Destroy","Destroying") -self:AddTransition("*","Return","Returning") -self.SetClient=SetClient -self.SetClient:FilterOnce() -self.SpawnAI=SpawnAI -self.SpawnQueue={} -self.ToNearestAirbase=false -self.ToHomeAirbase=false -self:__Monitor(1) -return self -end -function AI_BALANCER:InitSpawnInterval(Earliest,Latest) -self.Earliest=Earliest -self.Latest=Latest -return self -end -function AI_BALANCER:ReturnToNearestAirbases(ReturnThresholdRange,ReturnAirbaseSet) -self.ToNearestAirbase=true -self.ReturnThresholdRange=ReturnThresholdRange -self.ReturnAirbaseSet=ReturnAirbaseSet -end -function AI_BALANCER:ReturnToHomeAirbase(ReturnThresholdRange) -self.ToHomeAirbase=true -self.ReturnThresholdRange=ReturnThresholdRange -end -function AI_BALANCER:onenterSpawning(SetGroup,From,Event,To,ClientName) -local AIGroup=self.SpawnAI:Spawn() -if AIGroup then -AIGroup:E("Spawning new AIGroup") -SetGroup:Add(ClientName,AIGroup) -self.SpawnQueue[ClientName]=nil -self:Spawned(AIGroup) -end -end -function AI_BALANCER:onenterDestroying(SetGroup,From,Event,To,ClientName,AIGroup) -AIGroup:Destroy() -SetGroup:Flush() -SetGroup:Remove(ClientName) -SetGroup:Flush() -end -function AI_BALANCER:onenterReturning(SetGroup,From,Event,To,AIGroup) -local AIGroupTemplate=AIGroup:GetTemplate() -if self.ToHomeAirbase==true then -local WayPointCount=#AIGroupTemplate.route.points -local SwitchWayPointCommand=AIGroup:CommandSwitchWayPoint(1,WayPointCount,1) -AIGroup:SetCommand(SwitchWayPointCommand) -AIGroup:MessageToRed("Returning to home base ...",30) -else -local PointVec2=POINT_VEC2:New(AIGroup:GetVec2().x,AIGroup:GetVec2().y) -local ClosestAirbase=self.ReturnAirbaseSet:FindNearestAirbaseFromPointVec2(PointVec2) -self:T(ClosestAirbase.AirbaseName) -AIGroup:MessageToRed("Returning to "..ClosestAirbase:GetName().." ...",30) -local RTBRoute=AIGroup:RouteReturnToAirbase(ClosestAirbase) -AIGroupTemplate.route=RTBRoute -AIGroup:Respawn(AIGroupTemplate) -end -end -function AI_BALANCER:onenterMonitoring(SetGroup) -self:T2({self.SetClient:Count()}) -self.SetClient:ForEachClient( -function(Client) -self:T3(Client.ClientName) -local AIGroup=self.Set:Get(Client.UnitName) -if Client:IsAlive()then -if AIGroup and AIGroup:IsAlive()==true then -if self.ToNearestAirbase==false and self.ToHomeAirbase==false then -self:Destroy(Client.UnitName,AIGroup) -else -local PlayerInRange={Value=false} -local RangeZone=ZONE_RADIUS:New('RangeZone',AIGroup:GetVec2(),self.ReturnThresholdRange) -self:T2(RangeZone) -_DATABASE:ForEachPlayer( -function(RangeTestUnit,RangeZone,AIGroup,PlayerInRange) -self:T2({PlayerInRange,RangeTestUnit.UnitName,RangeZone.ZoneName}) -if RangeTestUnit:IsInZone(RangeZone)==true then -self:T2("in zone") -if RangeTestUnit:GetCoalition()~=AIGroup:GetCoalition()then -self:T2("in range") -PlayerInRange.Value=true -end -end -end, -function(RangeZone,AIGroup,PlayerInRange) -if PlayerInRange.Value==false then -self:Return(AIGroup) -end -end -,RangeZone,AIGroup,PlayerInRange -) -end -self.Set:Remove(Client.UnitName) -end -else -if not AIGroup or not AIGroup:IsAlive()==true then -self:T("Client "..Client.UnitName.." not alive.") -if not self.SpawnQueue[Client.UnitName]then -self:__Spawn(math.random(self.Earliest,self.Latest),Client.UnitName) -self.SpawnQueue[Client.UnitName]=true -self:E("New AI Spawned for Client "..Client.UnitName) -end -end -end -return true -end -) -self:__Monitor(10) -end -AI_A2A={ -ClassName="AI_A2A", -} -function AI_A2A:New(AIGroup) -local self=BASE:Inherit(self,FSM_CONTROLLABLE:New()) -self:SetControllable(AIGroup) -self:SetFuelThreshold(.2,60) -self:SetDamageThreshold(0.4) -self:SetDisengageRadius(70000) -self:SetStartState("Stopped") -self:AddTransition("*","Start","Started") -self:AddTransition("*","Stop","Stopped") -self:AddTransition("*","Status","*") -self:AddTransition("*","RTB","*") -self:AddTransition("Patrolling","Refuel","Refuelling") -self:AddTransition("*","Takeoff","Airborne") -self:AddTransition("*","Return","Returning") -self:AddTransition("*","Hold","Holding") -self:AddTransition("*","Home","Home") -self:AddTransition("*","LostControl","LostControl") -self:AddTransition("*","Fuel","Fuel") -self:AddTransition("*","Damaged","Damaged") -self:AddTransition("*","Eject","*") -self:AddTransition("*","Crash","Crashed") -self:AddTransition("*","PilotDead","*") -self.IdleCount=0 -return self -end -function GROUP:OnEventTakeoff(EventData,Fsm) -Fsm:Takeoff() -self:UnHandleEvent(EVENTS.Takeoff) -end -function AI_A2A:SetDispatcher(Dispatcher) -self.Dispatcher=Dispatcher -end -function AI_A2A:GetDispatcher() -return self.Dispatcher -end -function AI_A2A:SetTargetDistance(Coordinate) -local CurrentCoord=self.Controllable:GetCoordinate() -self.TargetDistance=CurrentCoord:Get2DDistance(Coordinate) -self.ClosestTargetDistance=(not self.ClosestTargetDistance or self.ClosestTargetDistance>self.TargetDistance)and self.TargetDistance or self.ClosestTargetDistance -end -function AI_A2A:ClearTargetDistance() -self.TargetDistance=nil -self.ClosestTargetDistance=nil -end -function AI_A2A:SetSpeed(PatrolMinSpeed,PatrolMaxSpeed) -self:F2({PatrolMinSpeed,PatrolMaxSpeed}) -self.PatrolMinSpeed=PatrolMinSpeed -self.PatrolMaxSpeed=PatrolMaxSpeed -end -function AI_A2A:SetAltitude(PatrolFloorAltitude,PatrolCeilingAltitude) -self:F2({PatrolFloorAltitude,PatrolCeilingAltitude}) -self.PatrolFloorAltitude=PatrolFloorAltitude -self.PatrolCeilingAltitude=PatrolCeilingAltitude -end -function AI_A2A:SetHomeAirbase(HomeAirbase) -self:F2({HomeAirbase}) -self.HomeAirbase=HomeAirbase -end -function AI_A2A:SetTanker(TankerName) -self:F2({TankerName}) -self.TankerName=TankerName -end -function AI_A2A:SetDisengageRadius(DisengageRadius) -self:F2({DisengageRadius}) -self.DisengageRadius=DisengageRadius -end -function AI_A2A:SetStatusOff() -self:F2() -self.CheckStatus=false -end -function AI_A2A:SetFuelThreshold(PatrolFuelThresholdPercentage,PatrolOutOfFuelOrbitTime) -self.PatrolManageFuel=true -self.PatrolFuelThresholdPercentage=PatrolFuelThresholdPercentage -self.PatrolOutOfFuelOrbitTime=PatrolOutOfFuelOrbitTime -self.Controllable:OptionRTBBingoFuel(false) -return self -end -function AI_A2A:SetDamageThreshold(PatrolDamageThreshold) -self.PatrolManageDamage=true -self.PatrolDamageThreshold=PatrolDamageThreshold -return self -end -function AI_A2A:onafterStart(Controllable,From,Event,To) -self:F2() -self:__Status(10) -self:HandleEvent(EVENTS.PilotDead,self.OnPilotDead) -self:HandleEvent(EVENTS.Crash,self.OnCrash) -self:HandleEvent(EVENTS.Ejection,self.OnEjection) -Controllable:OptionROEHoldFire() -Controllable:OptionROTVertical() -end -function AI_A2A:onbeforeStatus() -return self.CheckStatus -end -function AI_A2A:onafterStatus() -self:F(" Checking Status") -if self.Controllable and self.Controllable:IsAlive()then -local RTB=false -local DistanceFromHomeBase=self.HomeAirbase:GetCoordinate():Get2DDistance(self.Controllable:GetCoordinate()) -if not self:Is("Holding")and not self:Is("Returning")then -local DistanceFromHomeBase=self.HomeAirbase:GetCoordinate():Get2DDistance(self.Controllable:GetCoordinate()) -self:F({DistanceFromHomeBase=DistanceFromHomeBase}) -if DistanceFromHomeBase>self.DisengageRadius then -self:E(self.Controllable:GetName().." is too far from home base, RTB!") -self:Hold(300) -RTB=false -end -end -if self:Is("Fuel")or self:Is("Damaged")or self:Is("LostControl")then -if DistanceFromHomeBase<5000 then -self:E(self.Controllable:GetName().." is too far from home base, RTB!") -self:Home("Destroy") -end -end -if not self:Is("Fuel")and not self:Is("Home")then -local Fuel=self.Controllable:GetFuel() -self:F({Fuel=Fuel}) -if Fuel=2 then -if Damage~=InitialLife then -self:Damaged() -else -self:E(self.Controllable:GetName().." control lost! ") -self:LostControl() -end -else -self.IdleCount=self.IdleCount+1 -end -end -else -self.IdleCount=0 -end -if RTB==true then -self:__RTB(0.5) -end -self:__Status(10) -end -end -function AI_A2A.RTBRoute(AIGroup,Fsm) -AIGroup:F({"AI_A2A.RTBRoute:",AIGroup:GetName()}) -if AIGroup:IsAlive()then -Fsm:__RTB(0.5) -end -end -function AI_A2A.RTBHold(AIGroup,Fsm) -AIGroup:F({"AI_A2A.RTBHold:",AIGroup:GetName()}) -if AIGroup:IsAlive()then -Fsm:__RTB(0.5) -Fsm:Return() -local Task=AIGroup:TaskOrbitCircle(4000,400) -AIGroup:SetTask(Task) -end -end -function AI_A2A:onafterRTB(AIGroup,From,Event,To) -self:F({AIGroup,From,Event,To}) -if AIGroup and AIGroup:IsAlive()then -self:E("Group "..AIGroup:GetName().." ... RTB! ( "..self:GetState().." )") -self:ClearTargetDistance() -AIGroup:ClearTasks() -local EngageRoute={} -local CurrentCoord=AIGroup:GetCoordinate() -local ToTargetCoord=self.HomeAirbase:GetCoordinate() -local ToTargetSpeed=math.random(self.PatrolMinSpeed,self.PatrolMaxSpeed) -local ToAirbaseAngle=CurrentCoord:GetAngleDegrees(CurrentCoord:GetDirectionVec3(ToTargetCoord)) -local Distance=CurrentCoord:Get2DDistance(ToTargetCoord) -local ToAirbaseCoord=CurrentCoord:Translate(5000,ToAirbaseAngle) -if Distance<5000 then -self:E("RTB and near the airbase!") -self:Home() -return -end -local ToRTBRoutePoint=ToAirbaseCoord:WaypointAir( -self.PatrolAltType, -POINT_VEC3.RoutePointType.TurningPoint, -POINT_VEC3.RoutePointAction.TurningPoint, -ToTargetSpeed, -true -) -self:F({Angle=ToAirbaseAngle,ToTargetSpeed=ToTargetSpeed}) -self:T2({self.MinSpeed,self.MaxSpeed,ToTargetSpeed}) -EngageRoute[#EngageRoute+1]=ToRTBRoutePoint -EngageRoute[#EngageRoute+1]=ToRTBRoutePoint -AIGroup:OptionROEHoldFire() -AIGroup:OptionROTEvadeFire() -AIGroup:WayPointInitialize(EngageRoute) -local Tasks={} -Tasks[#Tasks+1]=AIGroup:TaskFunction("AI_A2A.RTBRoute",self) -EngageRoute[#EngageRoute].task=AIGroup:TaskCombo(Tasks) -AIGroup:Route(EngageRoute,0.5) -end -end -function AI_A2A:onafterHome(AIGroup,From,Event,To) -self:F({AIGroup,From,Event,To}) -self:E("Group "..self.Controllable:GetName().." ... Home! ( "..self:GetState().." )") -if AIGroup and AIGroup:IsAlive()then -end -end -function AI_A2A:onafterHold(AIGroup,From,Event,To,HoldTime) -self:F({AIGroup,From,Event,To}) -self:E("Group "..self.Controllable:GetName().." ... Holding! ( "..self:GetState().." )") -if AIGroup and AIGroup:IsAlive()then -local OrbitTask=AIGroup:TaskOrbitCircle(math.random(self.PatrolFloorAltitude,self.PatrolCeilingAltitude),self.PatrolMinSpeed) -local TimedOrbitTask=AIGroup:TaskControlled(OrbitTask,AIGroup:TaskCondition(nil,nil,nil,nil,HoldTime,nil)) -local RTBTask=AIGroup:TaskFunction("AI_A2A.RTBHold",self) -local OrbitHoldTask=AIGroup:TaskOrbitCircle(4000,self.PatrolMinSpeed) -AIGroup:SetTask(AIGroup:TaskCombo({TimedOrbitTask,RTBTask,OrbitHoldTask}),1) -end -end -function AI_A2A.Resume(AIGroup,Fsm) -AIGroup:F({"AI_A2A.Resume:",AIGroup:GetName()}) -if AIGroup:IsAlive()then -Fsm:__RTB(0.5) -end -end -function AI_A2A:onafterRefuel(AIGroup,From,Event,To) -self:F({AIGroup,From,Event,To}) -self:E("Group "..self.Controllable:GetName().." ... Refuelling! ( "..self:GetState().." )") -if AIGroup and AIGroup:IsAlive()then -local Tanker=GROUP:FindByName(self.TankerName) -if Tanker:IsAlive()and Tanker:IsAirPlane()then -local RefuelRoute={} -local CurrentCoord=AIGroup:GetCoordinate() -local ToRefuelCoord=Tanker:GetCoordinate() -local ToRefuelSpeed=math.random(self.PatrolMinSpeed,self.PatrolMaxSpeed) -local ToRefuelRoutePoint=ToRefuelCoord:WaypointAir( -self.PatrolAltType, -POINT_VEC3.RoutePointType.TurningPoint, -POINT_VEC3.RoutePointAction.TurningPoint, -ToRefuelSpeed, -true -) -self:F({ToRefuelSpeed=ToRefuelSpeed}) -RefuelRoute[#RefuelRoute+1]=ToRefuelRoutePoint -RefuelRoute[#RefuelRoute+1]=ToRefuelRoutePoint -AIGroup:OptionROEHoldFire() -AIGroup:OptionROTEvadeFire() -local Tasks={} -Tasks[#Tasks+1]=AIGroup:TaskRefueling() -Tasks[#Tasks+1]=AIGroup:TaskFunction(self:GetClassName()..".Resume",self) -RefuelRoute[#RefuelRoute].task=AIGroup:TaskCombo(Tasks) -AIGroup:Route(RefuelRoute,0.5) -else -self:RTB() -end -end -end -function AI_A2A:onafterDead() -self:SetStatusOff() -end -function AI_A2A:OnCrash(EventData) -if self.Controllable:IsAlive()and EventData.IniDCSGroupName==self.Controllable:GetName()then -self:E(self.Controllable:GetUnits()) -if#self.Controllable:GetUnits()==1 then -self:__Crash(1,EventData) -end -end -end -function AI_A2A:OnEjection(EventData) -if self.Controllable:IsAlive()and EventData.IniDCSGroupName==self.Controllable:GetName()then -self:__Eject(1,EventData) -end -end -function AI_A2A:OnPilotDead(EventData) -if self.Controllable:IsAlive()and EventData.IniDCSGroupName==self.Controllable:GetName()then -self:__PilotDead(1,EventData) -end -end -AI_A2A_PATROL={ -ClassName="AI_A2A_PATROL", -} -function AI_A2A_PATROL:New(AIPatrol,PatrolZone,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,PatrolAltType) -local self=BASE:Inherit(self,AI_A2A:New(AIPatrol)) -self.PatrolZone=PatrolZone -self.PatrolFloorAltitude=PatrolFloorAltitude -self.PatrolCeilingAltitude=PatrolCeilingAltitude -self.PatrolMinSpeed=PatrolMinSpeed -self.PatrolMaxSpeed=PatrolMaxSpeed -self.PatrolAltType=PatrolAltType or"RADIO" -self:AddTransition({"Started","Airborne","Refuelling"},"Patrol","Patrolling") -self:AddTransition("Patrolling","Route","Patrolling") -self:AddTransition("*","Reset","Patrolling") -return self -end -function AI_A2A_PATROL:SetSpeed(PatrolMinSpeed,PatrolMaxSpeed) -self:F2({PatrolMinSpeed,PatrolMaxSpeed}) -self.PatrolMinSpeed=PatrolMinSpeed -self.PatrolMaxSpeed=PatrolMaxSpeed -end -function AI_A2A_PATROL:SetAltitude(PatrolFloorAltitude,PatrolCeilingAltitude) -self:F2({PatrolFloorAltitude,PatrolCeilingAltitude}) -self.PatrolFloorAltitude=PatrolFloorAltitude -self.PatrolCeilingAltitude=PatrolCeilingAltitude -end -function AI_A2A_PATROL:onafterPatrol(AIPatrol,From,Event,To) -self:F2() -self:ClearTargetDistance() -self:__Route(1) -self.AIPatrol:OnReSpawn( -function(PatrolGroup) -self:E("ReSpawn") -self:__Reset(1) -self:__Route(5) -end -) -end -function AI_A2A_PATROL.PatrolRoute(AIPatrol,Fsm) -AIPatrol:F({"AI_A2A_PATROL.PatrolRoute:",AIPatrol:GetName()}) -if AIPatrol:IsAlive()then -Fsm:Route() -end -end -function AI_A2A_PATROL:onafterRoute(AIPatrol,From,Event,To) -self:F2() -if From=="RTB"then -return -end -if AIPatrol:IsAlive()then -local PatrolRoute={} -local CurrentCoord=AIPatrol:GetCoordinate() -local ToTargetCoord=self.PatrolZone:GetRandomPointVec2() -ToTargetCoord:SetAlt(math.random(self.PatrolFloorAltitude,self.PatrolCeilingAltitude)) -self:SetTargetDistance(ToTargetCoord) -local ToTargetSpeed=math.random(self.PatrolMinSpeed,self.PatrolMaxSpeed) -local ToPatrolRoutePoint=ToTargetCoord:WaypointAir( -self.PatrolAltType, -POINT_VEC3.RoutePointType.TurningPoint, -POINT_VEC3.RoutePointAction.TurningPoint, -ToTargetSpeed, -true -) -PatrolRoute[#PatrolRoute+1]=ToPatrolRoutePoint -PatrolRoute[#PatrolRoute+1]=ToPatrolRoutePoint -local Tasks={} -Tasks[#Tasks+1]=AIPatrol:TaskFunction("AI_A2A_PATROL.PatrolRoute",self) -PatrolRoute[#PatrolRoute].task=AIPatrol:TaskCombo(Tasks) -AIPatrol:OptionROEReturnFire() -AIPatrol:OptionROTEvadeFire() -AIPatrol:Route(PatrolRoute,0.5) -end -end -function AI_A2A_PATROL.Resume(AIPatrol) -AIPatrol:F({"AI_A2A_PATROL.Resume:",AIPatrol:GetName()}) -if AIPatrol:IsAlive()then -local _AI_A2A=AIPatrol:GetState(AIPatrol,"AI_A2A") -_AI_A2A:__Reset(1) -_AI_A2A:__Route(5) -end -end -AI_A2A_CAP={ -ClassName="AI_A2A_CAP", -} -function AI_A2A_CAP:New(AICap,PatrolZone,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,EngageMinSpeed,EngageMaxSpeed,PatrolAltType) -local self=BASE:Inherit(self,AI_A2A_PATROL:New(AICap,PatrolZone,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,PatrolAltType)) -self.Accomplished=false -self.Engaging=false -self.EngageMinSpeed=EngageMinSpeed -self.EngageMaxSpeed=EngageMaxSpeed -self:AddTransition({"Patrolling","Engaging","Returning","Airborne"},"Engage","Engaging") -self:AddTransition("Engaging","Fired","Engaging") -self:AddTransition("*","Destroy","*") -self:AddTransition("Engaging","Abort","Patrolling") -self:AddTransition("Engaging","Accomplish","Patrolling") -return self -end -function AI_A2A_CAP:onafterStart(AICap,From,Event,To) -AICap:HandleEvent(EVENTS.Takeoff,nil,self) -end -function AI_A2A_CAP:SetEngageZone(EngageZone) -self:F2() -if EngageZone then -self.EngageZone=EngageZone -else -self.EngageZone=nil -end -end -function AI_A2A_CAP:SetEngageRange(EngageRange) -self:F2() -if EngageRange then -self.EngageRange=EngageRange -else -self.EngageRange=nil -end -end -function AI_A2A_CAP:onafterPatrol(AICap,From,Event,To) -self:GetParent(self).onafterPatrol(self,AICap,From,Event,To) -self:HandleEvent(EVENTS.Dead) -end -function AI_A2A_CAP.AttackRoute(AICap,Fsm) -AICap:F({"AI_A2A_CAP.AttackRoute:",AICap:GetName()}) -if AICap:IsAlive()then -Fsm:__Engage(0.5) -end -end -function AI_A2A_CAP:onbeforeEngage(AICap,From,Event,To) -if self.Accomplished==true then -return false -end -end -function AI_A2A_CAP:onafterAbort(AICap,From,Event,To) -AICap:ClearTasks() -self:__Route(0.5) -end -function AI_A2A_CAP:onafterEngage(AICap,From,Event,To,AttackSetUnit) -self:F({AICap,From,Event,To,AttackSetUnit}) -self.AttackSetUnit=AttackSetUnit or self.AttackSetUnit -local FirstAttackUnit=self.AttackSetUnit:GetFirst() -if FirstAttackUnit and FirstAttackUnit:IsAlive()then -if AICap:IsAlive()then -local EngageRoute={} -local CurrentCoord=AICap:GetCoordinate() -local ToTargetCoord=self.AttackSetUnit:GetFirst():GetCoordinate() -local ToTargetSpeed=math.random(self.EngageMinSpeed,self.EngageMaxSpeed) -local ToInterceptAngle=CurrentCoord:GetAngleDegrees(CurrentCoord:GetDirectionVec3(ToTargetCoord)) -local ToPatrolRoutePoint=CurrentCoord:Translate(5000,ToInterceptAngle):WaypointAir( -self.PatrolAltType, -POINT_VEC3.RoutePointType.TurningPoint, -POINT_VEC3.RoutePointAction.TurningPoint, -ToTargetSpeed, -true -) -self:F({Angle=ToInterceptAngle,ToTargetSpeed=ToTargetSpeed}) -self:T2({self.MinSpeed,self.MaxSpeed,ToTargetSpeed}) -EngageRoute[#EngageRoute+1]=ToPatrolRoutePoint -EngageRoute[#EngageRoute+1]=ToPatrolRoutePoint -local AttackTasks={} -for AttackUnitID,AttackUnit in pairs(self.AttackSetUnit:GetSet())do -local AttackUnit=AttackUnit -self:T({"Attacking Unit:",AttackUnit:GetName(),AttackUnit:IsAlive(),AttackUnit:IsAir()}) -if AttackUnit:IsAlive()and AttackUnit:IsAir()then -AttackTasks[#AttackTasks+1]=AICap:TaskAttackUnit(AttackUnit) -end -end -if#AttackTasks==0 then -self:E("No targets found -> Going back to Patrolling") -self:__Abort(0.5) -else -AICap:OptionROEOpenFire() -AICap:OptionROTEvadeFire() -AttackTasks[#AttackTasks+1]=AICap:TaskFunction("AI_A2A_CAP.AttackRoute",self) -EngageRoute[#EngageRoute].task=AICap:TaskCombo(AttackTasks) -end -AICap:Route(EngageRoute,0.5) -end -else -self:E("No targets found -> Going back to Patrolling") -self:__Abort(0.5) -end -end -function AI_A2A_CAP:onafterAccomplish(AICap,From,Event,To) -self.Accomplished=true -self:SetDetectionOff() -end -function AI_A2A_CAP:onafterDestroy(AICap,From,Event,To,EventData) -if EventData.IniUnit then -self.AttackUnits[EventData.IniUnit]=nil -end -end -function AI_A2A_CAP:OnEventDead(EventData) -self:F({"EventDead",EventData}) -if EventData.IniDCSUnit then -if self.AttackUnits and self.AttackUnits[EventData.IniUnit]then -self:__Destroy(1,EventData) -end -end -end -function AI_A2A_CAP.Resume(AICap) -AICap:F({"AI_A2A_CAP.Resume:",AICap:GetName()}) -if AICap:IsAlive()then -local _AI_A2A=AICap:GetState(AICap,"AI_A2A") -_AI_A2A:__Reset(1) -_AI_A2A:__Route(5) -end -end -AI_A2A_GCI={ -ClassName="AI_A2A_GCI", -} -function AI_A2A_GCI:New(AIIntercept,EngageMinSpeed,EngageMaxSpeed) -local self=BASE:Inherit(self,AI_A2A:New(AIIntercept)) -self.Accomplished=false -self.Engaging=false -self.EngageMinSpeed=EngageMinSpeed -self.EngageMaxSpeed=EngageMaxSpeed -self.PatrolMinSpeed=EngageMinSpeed -self.PatrolMaxSpeed=EngageMaxSpeed -self.PatrolAltType="RADIO" -self:AddTransition({"Started","Engaging","Returning","Airborne"},"Engage","Engaging") -self:AddTransition("Engaging","Fired","Engaging") -self:AddTransition("*","Destroy","*") -self:AddTransition("Engaging","Abort","Patrolling") -self:AddTransition("Engaging","Accomplish","Patrolling") -return self -end -function AI_A2A_GCI:onafterStart(AIIntercept,From,Event,To) -AIIntercept:HandleEvent(EVENTS.Takeoff,nil,self) -end -function AI_A2A_GCI:onafterEngage(AIIntercept,From,Event,To) -self:HandleEvent(EVENTS.Dead) -end -function AI_A2A_GCI.InterceptRoute(AIIntercept,Fsm) -AIIntercept:F({"AI_A2A_GCI.InterceptRoute:",AIIntercept:GetName()}) -if AIIntercept:IsAlive()then -Fsm:__Engage(0.5) -end -end -function AI_A2A_GCI:onbeforeEngage(AIIntercept,From,Event,To) -if self.Accomplished==true then -return false -end -end -function AI_A2A_GCI:onafterAbort(AIIntercept,From,Event,To) -AIIntercept:ClearTasks() -self:Return() -self:__RTB(0.5) -end -function AI_A2A_GCI:onafterEngage(AIIntercept,From,Event,To,AttackSetUnit) -self:F({AIIntercept,From,Event,To,AttackSetUnit}) -self.AttackSetUnit=AttackSetUnit or self.AttackSetUnit -local FirstAttackUnit=self.AttackSetUnit:GetFirst() -if FirstAttackUnit and FirstAttackUnit:IsAlive()then -if AIIntercept:IsAlive()then -local EngageRoute={} -local CurrentCoord=AIIntercept:GetCoordinate() -local CurrentCoord=AIIntercept:GetCoordinate() -local ToTargetCoord=self.AttackSetUnit:GetFirst():GetCoordinate() -self:SetTargetDistance(ToTargetCoord) -local ToTargetSpeed=math.random(self.EngageMinSpeed,self.EngageMaxSpeed) -local ToInterceptAngle=CurrentCoord:GetAngleDegrees(CurrentCoord:GetDirectionVec3(ToTargetCoord)) -local ToPatrolRoutePoint=CurrentCoord:Translate(15000,ToInterceptAngle):WaypointAir( -self.PatrolAltType, -POINT_VEC3.RoutePointType.TurningPoint, -POINT_VEC3.RoutePointAction.TurningPoint, -ToTargetSpeed, -true -) -self:F({Angle=ToInterceptAngle,ToTargetSpeed=ToTargetSpeed}) -self:F({self.EngageMinSpeed,self.EngageMaxSpeed,ToTargetSpeed}) -EngageRoute[#EngageRoute+1]=ToPatrolRoutePoint -EngageRoute[#EngageRoute+1]=ToPatrolRoutePoint -local AttackTasks={} -for AttackUnitID,AttackUnit in pairs(self.AttackSetUnit:GetSet())do -local AttackUnit=AttackUnit -if AttackUnit:IsAlive()and AttackUnit:IsAir()then -self:T({"Intercepting Unit:",AttackUnit:GetName(),AttackUnit:IsAlive(),AttackUnit:IsAir()}) -AttackTasks[#AttackTasks+1]=AIIntercept:TaskAttackUnit(AttackUnit) -end -end -if#AttackTasks==0 then -self:E("No targets found -> Going RTB") -self:Return() -self:__RTB(0.5) -else -AIIntercept:OptionROEOpenFire() -AIIntercept:OptionROTEvadeFire() -AttackTasks[#AttackTasks+1]=AIIntercept:TaskFunction("AI_A2A_GCI.InterceptRoute",self) -EngageRoute[#EngageRoute].task=AIIntercept:TaskCombo(AttackTasks) -end -AIIntercept:Route(EngageRoute,0.5) -end -else -self:E("No targets found -> Going RTB") -self:Return() -self:__RTB(0.5) -end -end -function AI_A2A_GCI:onafterAccomplish(AIIntercept,From,Event,To) -self.Accomplished=true -self:SetDetectionOff() -end -function AI_A2A_GCI:onafterDestroy(AIIntercept,From,Event,To,EventData) -if EventData.IniUnit then -self.AttackUnits[EventData.IniUnit]=nil -end -end -function AI_A2A_GCI:OnEventDead(EventData) -self:F({"EventDead",EventData}) -if EventData.IniDCSUnit then -if self.AttackUnits and self.AttackUnits[EventData.IniUnit]then -self:__Destroy(1,EventData) -end -end -end -do -AI_A2A_DISPATCHER={ -ClassName="AI_A2A_DISPATCHER", -Detection=nil, -} -AI_A2A_DISPATCHER.Takeoff=GROUP.Takeoff -AI_A2A_DISPATCHER.Landing={ -NearAirbase=1, -AtRunway=2, -AtEngineShutdown=3, -} -function AI_A2A_DISPATCHER:New(Detection) -local self=BASE:Inherit(self,DETECTION_MANAGER:New(nil,Detection)) -self.Detection=Detection -self.DefenderSquadrons={} -self.DefenderSpawns={} -self.DefenderTasks={} -self.DefenderDefault={} -self.Detection:FilterCategories({Unit.Category.AIRPLANE,Unit.Category.HELICOPTER}) -self.Detection:SetRefreshTimeInterval(30) -self:SetEngageRadius() -self:SetGciRadius() -self:SetIntercept(300) -self:SetDisengageRadius(300000) -self:SetDefaultTakeoff(AI_A2A_DISPATCHER.Takeoff.Air) -self:SetDefaultTakeoffInAirAltitude(500) -self:SetDefaultLanding(AI_A2A_DISPATCHER.Landing.NearAirbase) -self:SetDefaultOverhead(1) -self:SetDefaultGrouping(1) -self:SetDefaultFuelThreshold(0.15,0) -self:SetDefaultDamageThreshold(0.4) -self:SetDefaultCapTimeInterval(180,600) -self:SetDefaultCapLimit(1) -self:AddTransition("Started","Assign","Started") -self:AddTransition("*","CAP","*") -self:AddTransition("*","GCI","*") -self:AddTransition("*","ENGAGE","*") -self:HandleEvent(EVENTS.Crash,self.OnEventCrashOrDead) -self:HandleEvent(EVENTS.Dead,self.OnEventCrashOrDead) -self:HandleEvent(EVENTS.Land) -self:HandleEvent(EVENTS.EngineShutdown) -self:SetTacticalDisplay(false) -self:__Start(5) -return self -end -function AI_A2A_DISPATCHER:OnEventCrashOrDead(EventData) -self.Detection:ForgetDetectedUnit(EventData.IniUnitName) -end -function AI_A2A_DISPATCHER:OnEventLand(EventData) -self:E("Landed") -local DefenderUnit=EventData.IniUnit -local Defender=EventData.IniGroup -local Squadron=self:GetSquadronFromDefender(Defender) -if Squadron then -self:F({SquadronName=Squadron.Name}) -local LandingMethod=self:GetSquadronLanding(Squadron.Name) -if LandingMethod==AI_A2A_DISPATCHER.Landing.AtRunway then -local DefenderSize=Defender:GetSize() -if DefenderSize==1 then -self:RemoveDefenderFromSquadron(Squadron,Defender) -end -DefenderUnit:Destroy() -return -end -if DefenderUnit:GetLife()~=DefenderUnit:GetLife0()then -DefenderUnit:Destroy() -return -end -if DefenderUnit:GetFuel()<=self.DefenderDefault.FuelThreshold then -DefenderUnit:Destroy() -return -end -end -end -function AI_A2A_DISPATCHER:OnEventEngineShutdown(EventData) -local DefenderUnit=EventData.IniUnit -local Defender=EventData.IniGroup -local Squadron=self:GetSquadronFromDefender(Defender) -if Squadron then -self:F({SquadronName=Squadron.Name}) -local LandingMethod=self:GetSquadronLanding(Squadron.Name) -if LandingMethod==AI_A2A_DISPATCHER.Landing.AtEngineShutdown then -local DefenderSize=Defender:GetSize() -if DefenderSize==1 then -self:RemoveDefenderFromSquadron(Squadron,Defender) -end -DefenderUnit:Destroy() -end -end -end -function AI_A2A_DISPATCHER:SetEngageRadius(EngageRadius) -self.Detection:SetFriendliesRange(EngageRadius or 100000) -return self -end -function AI_A2A_DISPATCHER:SetDisengageRadius(DisengageRadius) -self.DisengageRadius=DisengageRadius or 300000 -return self -end -function AI_A2A_DISPATCHER:SetGciRadius(GciRadius) -self.GciRadius=GciRadius or 200000 -return self -end -function AI_A2A_DISPATCHER:SetBorderZone(BorderZone) -self.Detection:SetAcceptZones(BorderZone) -return self -end -function AI_A2A_DISPATCHER:SetTacticalDisplay(TacticalDisplay) -self.TacticalDisplay=TacticalDisplay -return self -end -function AI_A2A_DISPATCHER:SetDefaultDamageThreshold(DamageThreshold) -self.DefenderDefault.DamageThreshold=DamageThreshold -return self -end -function AI_A2A_DISPATCHER:SetDefaultCapTimeInterval(CapMinSeconds,CapMaxSeconds) -self.DefenderDefault.CapMinSeconds=CapMinSeconds -self.DefenderDefault.CapMaxSeconds=CapMaxSeconds -return self -end -function AI_A2A_DISPATCHER:SetDefaultCapLimit(CapLimit) -self.DefenderDefault.CapLimit=CapLimit -return self -end -function AI_A2A_DISPATCHER:SetIntercept(InterceptDelay) -self.DefenderDefault.InterceptDelay=InterceptDelay -local Detection=self.Detection -Detection:SetIntercept(true,InterceptDelay) -return self -end -function AI_A2A_DISPATCHER:GetAIFriendliesNearBy(DetectedItem) -local FriendliesNearBy=self.Detection:GetFriendliesDistance(DetectedItem) -return FriendliesNearBy -end -function AI_A2A_DISPATCHER:GetDefenderTasks() -return self.DefenderTasks or{} -end -function AI_A2A_DISPATCHER:GetDefenderTask(Defender) -return self.DefenderTasks[Defender] -end -function AI_A2A_DISPATCHER:GetDefenderTaskFsm(Defender) -return self:GetDefenderTask(Defender).Fsm -end -function AI_A2A_DISPATCHER:GetDefenderTaskTarget(Defender) -return self:GetDefenderTask(Defender).Target -end -function AI_A2A_DISPATCHER:GetDefenderTaskSquadronName(Defender) -return self:GetDefenderTask(Defender).SquadronName -end -function AI_A2A_DISPATCHER:ClearDefenderTask(Defender) -if Defender:IsAlive()and self.DefenderTasks[Defender]then -local Target=self.DefenderTasks[Defender].Target -local Message="Clearing ("..self.DefenderTasks[Defender].Type..") " -Message=Message..Defender:GetName() -if Target then -Message=Message..(Target and(" from "..Target.Index.." ["..Target.Set:Count().."]"))or"" -end -self:F({Target=Message}) -end -self.DefenderTasks[Defender]=nil -return self -end -function AI_A2A_DISPATCHER:ClearDefenderTaskTarget(Defender) -local DefenderTask=self:GetDefenderTask(Defender) -if Defender:IsAlive()and DefenderTask then -local Target=DefenderTask.Target -local Message="Clearing ("..DefenderTask.Type..") " -Message=Message..Defender:GetName() -if Target then -Message=Message..(Target and(" from "..Target.Index.." ["..Target.Set:Count().."]"))or"" -end -self:F({Target=Message}) -end -if Defender and DefenderTask and DefenderTask.Target then -DefenderTask.Target=nil -end -return self -end -function AI_A2A_DISPATCHER:SetDefenderTask(SquadronName,Defender,Type,Fsm,Target) -self:F({SquadronName=SquadronName,Defender=Defender:GetName()}) -self.DefenderTasks[Defender]=self.DefenderTasks[Defender]or{} -self.DefenderTasks[Defender].Type=Type -self.DefenderTasks[Defender].Fsm=Fsm -self.DefenderTasks[Defender].SquadronName=SquadronName -if Target then -self:SetDefenderTaskTarget(Defender,Target) -end -return self -end -function AI_A2A_DISPATCHER:SetDefenderTaskTarget(Defender,AttackerDetection) -local Message="("..self.DefenderTasks[Defender].Type..") " -Message=Message..Defender:GetName() -Message=Message..(AttackerDetection and(" target "..AttackerDetection.Index.." ["..AttackerDetection.Set:Count().."]"))or"" -self:F({AttackerDetection=Message}) -if AttackerDetection then -self.DefenderTasks[Defender].Target=AttackerDetection -end -return self -end -function AI_A2A_DISPATCHER:SetSquadron(SquadronName,AirbaseName,TemplatePrefixes,Resources) -self.DefenderSquadrons[SquadronName]=self.DefenderSquadrons[SquadronName]or{} -local DefenderSquadron=self.DefenderSquadrons[SquadronName] -DefenderSquadron.Name=SquadronName -DefenderSquadron.Airbase=AIRBASE:FindByName(AirbaseName) -if not DefenderSquadron.Airbase then -error("Cannot find airbase with name:"..AirbaseName) -end -DefenderSquadron.Spawn={} -if type(TemplatePrefixes)=="string"then -local SpawnTemplate=TemplatePrefixes -self.DefenderSpawns[SpawnTemplate]=self.DefenderSpawns[SpawnTemplate]or SPAWN:New(SpawnTemplate) -DefenderSquadron.Spawn[1]=self.DefenderSpawns[SpawnTemplate] -else -for TemplateID,SpawnTemplate in pairs(TemplatePrefixes)do -self.DefenderSpawns[SpawnTemplate]=self.DefenderSpawns[SpawnTemplate]or SPAWN:New(SpawnTemplate) -DefenderSquadron.Spawn[#DefenderSquadron.Spawn+1]=self.DefenderSpawns[SpawnTemplate] -end -end -DefenderSquadron.Resources=Resources -DefenderSquadron.TemplatePrefixes=TemplatePrefixes -self:E({Squadron={SquadronName,AirbaseName,TemplatePrefixes,Resources}}) -return self -end -function AI_A2A_DISPATCHER:GetSquadron(SquadronName) -local DefenderSquadron=self.DefenderSquadrons[SquadronName] -if not DefenderSquadron then -error("Unknown Squadron:"..SquadronName) -end -return DefenderSquadron -end -function AI_A2A_DISPATCHER:SetSquadronCap(SquadronName,Zone,FloorAltitude,CeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,EngageMinSpeed,EngageMaxSpeed,AltType) -self.DefenderSquadrons[SquadronName]=self.DefenderSquadrons[SquadronName]or{} -self.DefenderSquadrons[SquadronName].Cap=self.DefenderSquadrons[SquadronName].Cap or{} -local DefenderSquadron=self:GetSquadron(SquadronName) -local Cap=self.DefenderSquadrons[SquadronName].Cap -Cap.Name=SquadronName -Cap.Zone=Zone -Cap.FloorAltitude=FloorAltitude -Cap.CeilingAltitude=CeilingAltitude -Cap.PatrolMinSpeed=PatrolMinSpeed -Cap.PatrolMaxSpeed=PatrolMaxSpeed -Cap.EngageMinSpeed=EngageMinSpeed -Cap.EngageMaxSpeed=EngageMaxSpeed -Cap.AltType=AltType -self:SetSquadronCapInterval(SquadronName,self.DefenderDefault.CapLimit,self.DefenderDefault.CapMinSeconds,self.DefenderDefault.CapMaxSeconds,1) -self:E({CAP={SquadronName,Zone,FloorAltitude,CeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,EngageMinSpeed,EngageMaxSpeed,AltType}}) -local RecceSet=self.Detection:GetDetectionSetGroup() -RecceSet:FilterPrefixes(DefenderSquadron.TemplatePrefixes) -RecceSet:FilterStart() -self.Detection:SetFriendlyPrefixes(DefenderSquadron.TemplatePrefixes) -return self -end -function AI_A2A_DISPATCHER:SetSquadronCapInterval(SquadronName,CapLimit,LowInterval,HighInterval,Probability) -self.DefenderSquadrons[SquadronName]=self.DefenderSquadrons[SquadronName]or{} -self.DefenderSquadrons[SquadronName].Cap=self.DefenderSquadrons[SquadronName].Cap or{} -local DefenderSquadron=self:GetSquadron(SquadronName) -local Cap=self.DefenderSquadrons[SquadronName].Cap -if Cap then -Cap.LowInterval=LowInterval or 180 -Cap.HighInterval=HighInterval or 600 -Cap.Probability=Probability or 1 -Cap.CapLimit=CapLimit or 1 -Cap.Scheduler=Cap.Scheduler or SCHEDULER:New(self) -local Scheduler=Cap.Scheduler -local ScheduleID=Cap.ScheduleID -local Variance=(Cap.HighInterval-Cap.LowInterval)/2 -local Repeat=Cap.LowInterval+Variance -local Randomization=Variance/Repeat -local Start=math.random(1,Cap.HighInterval) -if ScheduleID then -Scheduler:Stop(ScheduleID) -end -Cap.ScheduleID=Scheduler:Schedule(self,self.SchedulerCAP,{SquadronName},Start,Repeat,Randomization) -else -error("This squadron does not exist:"..SquadronName) -end -end -function AI_A2A_DISPATCHER:GetCAPDelay(SquadronName) -self.DefenderSquadrons[SquadronName]=self.DefenderSquadrons[SquadronName]or{} -self.DefenderSquadrons[SquadronName].Cap=self.DefenderSquadrons[SquadronName].Cap or{} -local DefenderSquadron=self:GetSquadron(SquadronName) -local Cap=self.DefenderSquadrons[SquadronName].Cap -if Cap then -return math.random(Cap.LowInterval,Cap.HighInterval) -else -error("This squadron does not exist:"..SquadronName) -end -end -function AI_A2A_DISPATCHER:CanCAP(SquadronName) -self:F({SquadronName=SquadronName}) -self.DefenderSquadrons[SquadronName]=self.DefenderSquadrons[SquadronName]or{} -self.DefenderSquadrons[SquadronName].Cap=self.DefenderSquadrons[SquadronName].Cap or{} -local DefenderSquadron=self:GetSquadron(SquadronName) -if(not DefenderSquadron.Resources)or(DefenderSquadron.Resources and DefenderSquadron.Resources>0)then -local Cap=DefenderSquadron.Cap -if Cap then -local CapCount=self:CountCapAirborne(SquadronName) -self:E({CapCount=CapCount}) -if CapCount0)then -local Gci=DefenderSquadron.Gci -if Gci then -return DefenderSquadron -end -end -return nil -end -function AI_A2A_DISPATCHER:SetSquadronGci(SquadronName,EngageMinSpeed,EngageMaxSpeed) -self.DefenderSquadrons[SquadronName]=self.DefenderSquadrons[SquadronName]or{} -self.DefenderSquadrons[SquadronName].Gci=self.DefenderSquadrons[SquadronName].Gci or{} -local Intercept=self.DefenderSquadrons[SquadronName].Gci -Intercept.Name=SquadronName -Intercept.EngageMinSpeed=EngageMinSpeed -Intercept.EngageMaxSpeed=EngageMaxSpeed -self:E({GCI={SquadronName,EngageMinSpeed,EngageMaxSpeed}}) -end -function AI_A2A_DISPATCHER:SetDefaultOverhead(Overhead) -self.DefenderDefault.Overhead=Overhead -return self -end -function AI_A2A_DISPATCHER:SetSquadronOverhead(SquadronName,Overhead) -local DefenderSquadron=self:GetSquadron(SquadronName) -DefenderSquadron.Overhead=Overhead -return self -end -function AI_A2A_DISPATCHER:SetDefaultGrouping(Grouping) -self.DefenderDefault.Grouping=Grouping -return self -end -function AI_A2A_DISPATCHER:SetSquadronGrouping(SquadronName,Grouping) -local DefenderSquadron=self:GetSquadron(SquadronName) -DefenderSquadron.Grouping=Grouping -return self -end -function AI_A2A_DISPATCHER:SetDefaultTakeoff(Takeoff) -self.DefenderDefault.Takeoff=Takeoff -return self -end -function AI_A2A_DISPATCHER:SetSquadronTakeoff(SquadronName,Takeoff) -local DefenderSquadron=self:GetSquadron(SquadronName) -DefenderSquadron.Takeoff=Takeoff -return self -end -function AI_A2A_DISPATCHER:GetDefaultTakeoff() -return self.DefenderDefault.Takeoff -end -function AI_A2A_DISPATCHER:GetSquadronTakeoff(SquadronName) -local DefenderSquadron=self:GetSquadron(SquadronName) -return DefenderSquadron.Takeoff or self.DefenderDefault.Takeoff -end -function AI_A2A_DISPATCHER:SetDefaultTakeoffInAir() -self:SetDefaultTakeoff(AI_A2A_DISPATCHER.Takeoff.Air) -return self -end -function AI_A2A_DISPATCHER:SetSquadronTakeoffInAir(SquadronName,TakeoffAltitude) -self:SetSquadronTakeoff(SquadronName,AI_A2A_DISPATCHER.Takeoff.Air) -if TakeoffAltitude then -self:SetSquadronTakeoffInAirAltitude(SquadronName,TakeoffAltitude) -end -return self -end -function AI_A2A_DISPATCHER:SetDefaultTakeoffFromRunway() -self:SetDefaultTakeoff(AI_A2A_DISPATCHER.Takeoff.Runway) -return self -end -function AI_A2A_DISPATCHER:SetSquadronTakeoffFromRunway(SquadronName) -self:SetSquadronTakeoff(SquadronName,AI_A2A_DISPATCHER.Takeoff.Runway) -return self -end -function AI_A2A_DISPATCHER:SetDefaultTakeoffFromParkingHot() -self:SetDefaultTakeoff(AI_A2A_DISPATCHER.Takeoff.Hot) -return self -end -function AI_A2A_DISPATCHER:SetSquadronTakeoffFromParkingHot(SquadronName) -self:SetSquadronTakeoff(SquadronName,AI_A2A_DISPATCHER.Takeoff.Hot) -return self -end -function AI_A2A_DISPATCHER:SetDefaultTakeoffFromParkingCold() -self:SetDefaultTakeoff(AI_A2A_DISPATCHER.Takeoff.Cold) -return self -end -function AI_A2A_DISPATCHER:SetSquadronTakeoffFromParkingCold(SquadronName) -self:SetSquadronTakeoff(SquadronName,AI_A2A_DISPATCHER.Takeoff.Cold) -return self -end -function AI_A2A_DISPATCHER:SetDefaultTakeoffInAirAltitude(TakeoffAltitude) -self.DefenderDefault.TakeoffAltitude=TakeoffAltitude -return self -end -function AI_A2A_DISPATCHER:SetSquadronTakeoffInAirAltitude(SquadronName,TakeoffAltitude) -local DefenderSquadron=self:GetSquadron(SquadronName) -DefenderSquadron.TakeoffAltitude=TakeoffAltitude -return self -end -function AI_A2A_DISPATCHER:SetDefaultLanding(Landing) -self.DefenderDefault.Landing=Landing -return self -end -function AI_A2A_DISPATCHER:SetSquadronLanding(SquadronName,Landing) -local DefenderSquadron=self:GetSquadron(SquadronName) -DefenderSquadron.Landing=Landing -return self -end -function AI_A2A_DISPATCHER:GetDefaultLanding() -return self.DefenderDefault.Landing -end -function AI_A2A_DISPATCHER:GetSquadronLanding(SquadronName) -local DefenderSquadron=self:GetSquadron(SquadronName) -return DefenderSquadron.Landing or self.DefenderDefault.Landing -end -function AI_A2A_DISPATCHER:SetDefaultLandingNearAirbase() -self:SetDefaultLanding(AI_A2A_DISPATCHER.Landing.NearAirbase) -return self -end -function AI_A2A_DISPATCHER:SetSquadronLandingNearAirbase(SquadronName) -self:SetSquadronLanding(SquadronName,AI_A2A_DISPATCHER.Landing.NearAirbase) -return self -end -function AI_A2A_DISPATCHER:SetDefaultLandingAtRunway() -self:SetDefaultLanding(AI_A2A_DISPATCHER.Landing.AtRunway) -return self -end -function AI_A2A_DISPATCHER:SetSquadronLandingAtRunway(SquadronName) -self:SetSquadronLanding(SquadronName,AI_A2A_DISPATCHER.Landing.AtRunway) -return self -end -function AI_A2A_DISPATCHER:SetDefaultLandingAtEngineShutdown() -self:SetDefaultLanding(AI_A2A_DISPATCHER.Landing.AtEngineShutdown) -return self -end -function AI_A2A_DISPATCHER:SetSquadronLandingAtEngineShutdown(SquadronName) -self:SetSquadronLanding(SquadronName,AI_A2A_DISPATCHER.Landing.AtEngineShutdown) -return self -end -function AI_A2A_DISPATCHER:SetDefaultFuelThreshold(FuelThreshold) -self.DefenderDefault.FuelThreshold=FuelThreshold -return self -end -function AI_A2A_DISPATCHER:SetSquadronFuelThreshold(SquadronName,FuelThreshold) -local DefenderSquadron=self:GetSquadron(SquadronName) -DefenderSquadron.FuelThreshold=FuelThreshold -return self -end -function AI_A2A_DISPATCHER:SetDefaultTanker(TankerName) -self.DefenderDefault.TankerName=TankerName -return self -end -function AI_A2A_DISPATCHER:SetSquadronTanker(SquadronName,TankerName) -local DefenderSquadron=self:GetSquadron(SquadronName) -DefenderSquadron.TankerName=TankerName -return self -end -function AI_A2A_DISPATCHER:AddDefenderToSquadron(Squadron,Defender,Size) -self.Defenders=self.Defenders or{} -local DefenderName=Defender:GetName() -self.Defenders[DefenderName]=Squadron -if Squadron.Resources then -Squadron.Resources=Squadron.Resources-Size -end -self:E({DefenderName=DefenderName,SquadronResources=Squadron.Resources}) -end -function AI_A2A_DISPATCHER:RemoveDefenderFromSquadron(Squadron,Defender) -self.Defenders=self.Defenders or{} -local DefenderName=Defender:GetName() -if Squadron.Resources then -Squadron.Resources=Squadron.Resources+Defender:GetSize() -end -self.Defenders[DefenderName]=nil -self:F({DefenderName=DefenderName,SquadronResources=Squadron.Resources}) -end -function AI_A2A_DISPATCHER:GetSquadronFromDefender(Defender) -self.Defenders=self.Defenders or{} -local DefenderName=Defender:GetName() -self:F({DefenderName=DefenderName}) -return self.Defenders[DefenderName] -end -function AI_A2A_DISPATCHER:EvaluateSWEEP(DetectedItem) -self:F({DetectedItem.ItemID}) -local DetectedSet=DetectedItem.Set -local DetectedZone=DetectedItem.Zone -if DetectedItem.IsDetected==false then -local TargetSetUnit=SET_UNIT:New() -TargetSetUnit:SetDatabase(DetectedSet) -TargetSetUnit:FilterOnce() -return TargetSetUnit -end -return nil -end -function AI_A2A_DISPATCHER:CountCapAirborne(SquadronName) -local CapCount=0 -local DefenderSquadron=self.DefenderSquadrons[SquadronName] -if DefenderSquadron then -for AIGroup,DefenderTask in pairs(self:GetDefenderTasks())do -if DefenderTask.SquadronName==SquadronName then -if DefenderTask.Type=="CAP"then -if AIGroup:IsAlive()then -if DefenderTask.Fsm:Is("Patrolling")or DefenderTask.Fsm:Is("Engaging")or DefenderTask.Fsm:Is("Refuelling")then -CapCount=CapCount+1 -end -end -end -end -end -end -return CapCount -end -function AI_A2A_DISPATCHER:CountDefendersEngaged(AttackerDetection) -local DefenderCount=0 -self:E("Counting Defenders Engaged for Attacker:") -local DetectedSet=AttackerDetection.Set -DetectedSet:Flush() -local DefenderTasks=self:GetDefenderTasks() -for DefenderGroup,DefenderTask in pairs(DefenderTasks)do -local Defender=DefenderGroup -local DefenderTaskTarget=DefenderTask.Target -local DefenderSquadronName=DefenderTask.SquadronName -if DefenderTaskTarget and DefenderTaskTarget.Index==AttackerDetection.Index then -local Squadron=self:GetSquadron(DefenderSquadronName) -local SquadronOverhead=Squadron.Overhead or self.DefenderDefault.Overhead -local DefenderSize=Defender:GetInitialSize() -DefenderCount=DefenderCount+DefenderSize/SquadronOverhead -self:F("Defender Group Name: "..Defender:GetName()..", Size: "..DefenderSize) -end -end -self:F({DefenderCount=DefenderCount}) -return DefenderCount -end -function AI_A2A_DISPATCHER:CountDefendersToBeEngaged(AttackerDetection,DefenderCount) -local Friendlies=nil -local AttackerSet=AttackerDetection.Set -local AttackerCount=AttackerSet:Count() -local DefenderFriendlies=self:GetAIFriendliesNearBy(AttackerDetection) -for FriendlyDistance,AIFriendly in UTILS.spairs(DefenderFriendlies or{})do -if AttackerCount>DefenderCount then -local Friendly=AIFriendly:GetGroup() -if Friendly and Friendly:IsAlive()then -local DefenderTask=self:GetDefenderTask(Friendly) -if DefenderTask then -if DefenderTask.Type=="CAP"or DefenderTask.Type=="GCI"then -if DefenderTask.Target==nil then -if DefenderTask.Fsm:Is("Returning") -or DefenderTask.Fsm:Is("Patrolling")then -Friendlies=Friendlies or{} -Friendlies[Friendly]=Friendly -DefenderCount=DefenderCount+Friendly:GetSize() -self:F({Friendly=Friendly:GetName(),FriendlyDistance=FriendlyDistance}) -end -end -end -end -end -else -break -end -end -return Friendlies -end -function AI_A2A_DISPATCHER:onafterCAP(From,Event,To,SquadronName) -self:F({SquadronName=SquadronName}) -self.DefenderSquadrons[SquadronName]=self.DefenderSquadrons[SquadronName]or{} -self.DefenderSquadrons[SquadronName].Cap=self.DefenderSquadrons[SquadronName].Cap or{} -local DefenderSquadron=self:CanCAP(SquadronName) -if DefenderSquadron then -local Cap=DefenderSquadron.Cap -if Cap then -local Spawn=DefenderSquadron.Spawn[math.random(1,#DefenderSquadron.Spawn)] -local DefenderGrouping=DefenderSquadron.Grouping or self.DefenderDefault.Grouping -Spawn:InitGrouping(DefenderGrouping) -local TakeoffMethod=self:GetSquadronTakeoff(SquadronName) -local DefenderCAP=Spawn:SpawnAtAirbase(DefenderSquadron.Airbase,TakeoffMethod,DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude) -self:AddDefenderToSquadron(DefenderSquadron,DefenderCAP,DefenderGrouping) -if DefenderCAP then -local Fsm=AI_A2A_CAP:New(DefenderCAP,Cap.Zone,Cap.FloorAltitude,Cap.CeilingAltitude,Cap.PatrolMinSpeed,Cap.PatrolMaxSpeed,Cap.EngageMinSpeed,Cap.EngageMaxSpeed,Cap.AltType) -Fsm:SetDispatcher(self) -Fsm:SetHomeAirbase(DefenderSquadron.Airbase) -Fsm:SetFuelThreshold(DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold,60) -Fsm:SetDamageThreshold(self.DefenderDefault.DamageThreshold) -Fsm:SetDisengageRadius(self.DisengageRadius) -Fsm:SetTanker(DefenderSquadron.TankerName or self.DefenderDefault.TankerName) -Fsm:Start() -self:SetDefenderTask(SquadronName,DefenderCAP,"CAP",Fsm) -function Fsm:onafterTakeoff(Defender,From,Event,To) -self:F({"GCI Birth",Defender:GetName()}) -local Dispatcher=Fsm:GetDispatcher() -local Squadron=Dispatcher:GetSquadronFromDefender(Defender) -if Squadron then -Fsm:__Patrol(2) -end -end -function Fsm:onafterRTB(Defender,From,Event,To) -self:F({"CAP RTB",Defender:GetName()}) -self:GetParent(self).onafterRTB(self,Defender,From,Event,To) -local Dispatcher=self:GetDispatcher() -Dispatcher:ClearDefenderTaskTarget(Defender) -end -function Fsm:onafterHome(Defender,From,Event,To,Action) -self:E({"CAP Home",Defender:GetName()}) -self:GetParent(self).onafterHome(self,Defender,From,Event,To) -local Dispatcher=self:GetDispatcher() -local Squadron=Dispatcher:GetSquadronFromDefender(Defender) -if Action and Action=="Destroy"then -Dispatcher:RemoveDefenderFromSquadron(Squadron,Defender) -Defender:Destroy() -end -if Dispatcher:GetSquadronLanding(Squadron.Name)==AI_A2A_DISPATCHER.Landing.NearAirbase then -Dispatcher:RemoveDefenderFromSquadron(Squadron,Defender) -Defender:Destroy() -end -end -end -end -end -end -function AI_A2A_DISPATCHER:onafterENGAGE(From,Event,To,AttackerDetection,Defenders) -if Defenders then -for DefenderID,Defender in pairs(Defenders)do -local Fsm=self:GetDefenderTaskFsm(Defender) -Fsm:__Engage(1,AttackerDetection.Set) -self:SetDefenderTaskTarget(Defender,AttackerDetection) -end -end -end -function AI_A2A_DISPATCHER:onafterGCI(From,Event,To,AttackerDetection,DefendersMissing,DefenderFriendlies) -self:F({From,Event,To,AttackerDetection.Index,DefendersMissing,DefenderFriendlies}) -local AttackerSet=AttackerDetection.Set -local AttackerUnit=AttackerSet:GetFirst() -if AttackerUnit and AttackerUnit:IsAlive()then -local AttackerCount=AttackerSet:Count() -local DefenderCount=0 -for DefenderID,DefenderGroup in pairs(DefenderFriendlies or{})do -local Fsm=self:GetDefenderTaskFsm(DefenderGroup) -Fsm:__Engage(1,AttackerSet) -self:SetDefenderTaskTarget(DefenderGroup,AttackerDetection) -DefenderCount=DefenderCount+DefenderGroup:GetSize() -end -self:F({DefenderCount=DefenderCount,DefendersMissing=DefendersMissing}) -DefenderCount=DefendersMissing -local ClosestDistance=0 -local ClosestDefenderSquadronName=nil -local BreakLoop=false -while(DefenderCount>0 and not BreakLoop)do -self:F({DefenderSquadrons=self.DefenderSquadrons}) -for SquadronName,DefenderSquadron in pairs(self.DefenderSquadrons or{})do -self:F({GCI=DefenderSquadron.Gci}) -for InterceptID,Intercept in pairs(DefenderSquadron.Gci or{})do -self:F({DefenderSquadron}) -local SpawnCoord=DefenderSquadron.Airbase:GetCoordinate() -local AttackerCoord=AttackerUnit:GetCoordinate() -local InterceptCoord=AttackerDetection.InterceptCoord -self:F({InterceptCoord=InterceptCoord}) -if InterceptCoord then -local InterceptDistance=SpawnCoord:Get2DDistance(InterceptCoord) -local AirbaseDistance=SpawnCoord:Get2DDistance(AttackerCoord) -self:F({InterceptDistance=InterceptDistance,AirbaseDistance=AirbaseDistance,InterceptCoord=InterceptCoord}) -if ClosestDistance==0 or InterceptDistanceDefenderSquadron.Resources then -DefendersNeeded=DefenderSquadron.Resources -BreakLoop=true -end -while(DefendersNeeded>0)do -local Spawn=DefenderSquadron.Spawn[math.random(1,#DefenderSquadron.Spawn)] -local DefenderGrouping=(DefenderGrouping0 then -for PlayerName,PlayerType in pairs(PlayerTypes)do -PlayerTypesReport:Add(string.format('"%s" in %s',PlayerName,PlayerType)) -end -else -PlayerTypesReport:Add("-") -end -return PlayersCount,PlayerTypesReport -end -function AI_A2A_DISPATCHER:GetFriendliesNearBy(Target) -local DetectedSet=Target.Set -local FriendlyUnitsNearBy=self.Detection:GetFriendliesNearBy(Target) -local FriendlyTypes={} -local FriendliesCount=0 -if FriendlyUnitsNearBy then -local DetectedTreatLevel=DetectedSet:CalculateThreatLevelA2G() -for FriendlyUnitName,FriendlyUnitData in pairs(FriendlyUnitsNearBy)do -local FriendlyUnit=FriendlyUnitData -if FriendlyUnit:IsAirPlane()then -local FriendlyUnitThreatLevel=FriendlyUnit:GetThreatLevel() -FriendliesCount=FriendliesCount+1 -local FriendlyType=FriendlyUnit:GetTypeName() -FriendlyTypes[FriendlyType]=FriendlyTypes[FriendlyType]and(FriendlyTypes[FriendlyType]+1)or 1 -if DetectedTreatLevel0 then -for FriendlyType,FriendlyTypeCount in pairs(FriendlyTypes)do -FriendlyTypesReport:Add(string.format("%d of %s",FriendlyTypeCount,FriendlyType)) -end -else -FriendlyTypesReport:Add("-") -end -return FriendliesCount,FriendlyTypesReport -end -function AI_A2A_DISPATCHER:SchedulerCAP(SquadronName) -self:CAP(SquadronName) -end -end -do -AI_A2A_GCICAP={ -ClassName="AI_A2A_GCICAP", -Detection=nil, -} -function AI_A2A_GCICAP:New(EWRPrefixes,TemplatePrefixes,CapPrefixes,CapLimit,GroupingRadius,EngageRadius,GciRadius,Resources) -local EWRSetGroup=SET_GROUP:New() -EWRSetGroup:FilterPrefixes(EWRPrefixes) -EWRSetGroup:FilterStart() -local Detection=DETECTION_AREAS:New(EWRSetGroup,GroupingRadius or 30000) -local self=BASE:Inherit(self,AI_A2A_DISPATCHER:New(Detection)) -self:SetEngageRadius(EngageRadius) -self:SetGciRadius(GciRadius) -local EWRFirst=EWRSetGroup:GetFirst() -local EWRCoalition=EWRFirst:GetCoalition() -local AirbaseNames={} -for AirbaseID,AirbaseData in pairs(_DATABASE.AIRBASES)do -local Airbase=AirbaseData -local AirbaseName=Airbase:GetName() -if Airbase:GetCoalition()==EWRCoalition then -table.insert(AirbaseNames,AirbaseName) -end -end -self.Templates=SET_GROUP -:New() -:FilterPrefixes(TemplatePrefixes) -:FilterOnce() -self:F({Airbases=AirbaseNames}) -self.Templates:Flush() -for AirbaseID,AirbaseName in pairs(AirbaseNames)do -local Airbase=_DATABASE:FindAirbase(AirbaseName) -local AirbaseName=Airbase:GetName() -local AirbaseCoord=Airbase:GetCoordinate() -local AirbaseZone=ZONE_RADIUS:New("Airbase",AirbaseCoord:GetVec2(),3000) -local Templates=nil -for TemplateID,Template in pairs(self.Templates:GetSet())do -local Template=Template -self:F({Template=Template:GetName()}) -local TemplateCoord=Template:GetCoordinate() -if AirbaseZone:IsVec2InZone(TemplateCoord:GetVec2())then -Templates=Templates or{} -table.insert(Templates,Template:GetName()) -end -end -if Templates then -self:SetSquadron(AirbaseName,AirbaseName,Templates,Resources) -end -end -self.CAPTemplates=SET_GROUP:New() -self.CAPTemplates:FilterPrefixes(CapPrefixes) -self.CAPTemplates:FilterOnce() -for CAPID,CAPTemplate in pairs(self.CAPTemplates:GetSet())do -local CAPZone=ZONE_POLYGON:New(CAPTemplate:GetName(),CAPTemplate) -local AirbaseDistance=99999999 -local AirbaseClosest=nil -for AirbaseID,AirbaseName in pairs(AirbaseNames)do -local Airbase=_DATABASE:FindAirbase(AirbaseName) -local AirbaseName=Airbase:GetName() -local AirbaseCoord=Airbase:GetCoordinate() -local Squadron=self.DefenderSquadrons[AirbaseName] -if Squadron then -local Distance=AirbaseCoord:Get2DDistance(CAPZone:GetCoordinate()) -if Distance Engaging') -self:__Engage(1) -end -end -end -function AI_CAP_ZONE:onafterAbort(Controllable,From,Event,To) -Controllable:ClearTasks() -self:__Route(1) -end -function AI_CAP_ZONE:onafterEngage(Controllable,From,Event,To) -if Controllable:IsAlive()then -local EngageRoute={} -local CurrentVec2=self.Controllable:GetVec2() -local CurrentAltitude=self.Controllable:GetUnit(1):GetAltitude() -local CurrentPointVec3=POINT_VEC3:New(CurrentVec2.x,CurrentAltitude,CurrentVec2.y) -local ToEngageZoneSpeed=self.PatrolMaxSpeed -local CurrentRoutePoint=CurrentPointVec3:WaypointAir( -self.PatrolAltType, -POINT_VEC3.RoutePointType.TurningPoint, -POINT_VEC3.RoutePointAction.TurningPoint, -ToEngageZoneSpeed, -true -) -EngageRoute[#EngageRoute+1]=CurrentRoutePoint -local ToTargetVec2=self.PatrolZone:GetRandomVec2() -self:T2(ToTargetVec2) -local ToTargetAltitude=math.random(self.EngageFloorAltitude,self.EngageCeilingAltitude) -local ToTargetSpeed=math.random(self.PatrolMinSpeed,self.PatrolMaxSpeed) -self:T2({self.PatrolMinSpeed,self.PatrolMaxSpeed,ToTargetSpeed}) -local ToTargetPointVec3=POINT_VEC3:New(ToTargetVec2.x,ToTargetAltitude,ToTargetVec2.y) -local ToPatrolRoutePoint=ToTargetPointVec3:WaypointAir( -self.PatrolAltType, -POINT_VEC3.RoutePointType.TurningPoint, -POINT_VEC3.RoutePointAction.TurningPoint, -ToTargetSpeed, -true -) -EngageRoute[#EngageRoute+1]=ToPatrolRoutePoint -Controllable:OptionROEOpenFire() -Controllable:OptionROTEvadeFire() -local AttackTasks={} -for DetectedUnit,Detected in pairs(self.DetectedUnits)do -local DetectedUnit=DetectedUnit -self:T({DetectedUnit,DetectedUnit:IsAlive(),DetectedUnit:IsAir()}) -if DetectedUnit:IsAlive()and DetectedUnit:IsAir()then -if self.EngageZone then -if DetectedUnit:IsInZone(self.EngageZone)then -self:F({"Within Zone and Engaging ",DetectedUnit}) -AttackTasks[#AttackTasks+1]=Controllable:TaskAttackUnit(DetectedUnit) -end -else -if self.EngageRange then -if DetectedUnit:GetPointVec3():Get2DDistance(Controllable:GetPointVec3())<=self.EngageRange then -self:F({"Within Range and Engaging",DetectedUnit}) -AttackTasks[#AttackTasks+1]=Controllable:TaskAttackUnit(DetectedUnit) -end -else -AttackTasks[#AttackTasks+1]=Controllable:TaskAttackUnit(DetectedUnit) -end -end -else -self.DetectedUnits[DetectedUnit]=nil -end -end -if#AttackTasks==0 then -self:F("No targets found -> Going back to Patrolling") -self:__Abort(1) -self:__Route(1) -self:SetDetectionActivated() -else -AttackTasks[#AttackTasks+1]=Controllable:TaskFunction("AI_CAP_ZONE.EngageRoute",self) -EngageRoute[1].task=Controllable:TaskCombo(AttackTasks) -self:SetDetectionDeactivated() -end -Controllable:Route(EngageRoute,0.5) -end -end -function AI_CAP_ZONE:onafterAccomplish(Controllable,From,Event,To) -self.Accomplished=true -self:SetDetectionOff() -end -function AI_CAP_ZONE:onafterDestroy(Controllable,From,Event,To,EventData) -if EventData.IniUnit then -self.DetectedUnits[EventData.IniUnit]=nil -end -end -function AI_CAP_ZONE:OnEventDead(EventData) -self:F({"EventDead",EventData}) -if EventData.IniDCSUnit then -if self.DetectedUnits and self.DetectedUnits[EventData.IniUnit]then -self:__Destroy(1,EventData) -end -end -end -AI_CAS_ZONE={ -ClassName="AI_CAS_ZONE", -} -function AI_CAS_ZONE:New(PatrolZone,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,EngageZone,PatrolAltType) -local self=BASE:Inherit(self,AI_PATROL_ZONE:New(PatrolZone,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,PatrolAltType)) -self.EngageZone=EngageZone -self.Accomplished=false -self:SetDetectionZone(self.EngageZone) -self:AddTransition({"Patrolling","Engaging"},"Engage","Engaging") -self:AddTransition("Engaging","Target","Engaging") -self:AddTransition("Engaging","Fired","Engaging") -self:AddTransition("*","Destroy","*") -self:AddTransition("Engaging","Abort","Patrolling") -self:AddTransition("Engaging","Accomplish","Patrolling") -return self -end -function AI_CAS_ZONE:SetEngageZone(EngageZone) -self:F2() -if EngageZone then -self.EngageZone=EngageZone -else -self.EngageZone=nil -end -end -function AI_CAS_ZONE:onafterStart(Controllable,From,Event,To) -self:GetParent(self).onafterStart(self,Controllable,From,Event,To) -self:HandleEvent(EVENTS.Dead) -self:SetDetectionDeactivated() -end -function AI_CAS_ZONE.EngageRoute(EngageGroup,Fsm) -EngageGroup:F({"AI_CAS_ZONE.EngageRoute:",EngageGroup:GetName()}) -if EngageGroup:IsAlive()then -Fsm:__Engage(1,Fsm.EngageSpeed,Fsm.EngageAltitude,Fsm.EngageWeaponExpend,Fsm.EngageAttackQty,Fsm.EngageDirection) -end -end -function AI_CAS_ZONE:onbeforeEngage(Controllable,From,Event,To) -if self.Accomplished==true then -return false -end -end -function AI_CAS_ZONE:onafterTarget(Controllable,From,Event,To) -self:E("onafterTarget") -if Controllable:IsAlive()then -local AttackTasks={} -for DetectedUnit,Detected in pairs(self.DetectedUnits)do -local DetectedUnit=DetectedUnit -if DetectedUnit:IsAlive()then -if DetectedUnit:IsInZone(self.EngageZone)then -if Detected==true then -self:E({"Target: ",DetectedUnit}) -self.DetectedUnits[DetectedUnit]=false -local AttackTask=Controllable:TaskAttackUnit(DetectedUnit,false,self.EngageWeaponExpend,self.EngageAttackQty,self.EngageDirection,self.EngageAltitude,nil) -self.Controllable:PushTask(AttackTask,1) -end -end -else -self.DetectedUnits[DetectedUnit]=nil -end -end -self:__Target(-10) -end -end -function AI_CAS_ZONE:onafterAbort(Controllable,From,Event,To) -Controllable:ClearTasks() -self:__Route(1) -end -function AI_CAS_ZONE:onafterEngage(Controllable,From,Event,To, -EngageSpeed, -EngageAltitude, -EngageWeaponExpend, -EngageAttackQty, -EngageDirection) -self:F("onafterEngage") -self.EngageSpeed=EngageSpeed or 400 -self.EngageAltitude=EngageAltitude or 2000 -self.EngageWeaponExpend=EngageWeaponExpend -self.EngageAttackQty=EngageAttackQty -self.EngageDirection=EngageDirection -if Controllable:IsAlive()then -Controllable:OptionROEOpenFire() -Controllable:OptionROTVertical() -local EngageRoute={} -local CurrentVec2=self.Controllable:GetVec2() -local CurrentAltitude=self.Controllable:GetUnit(1):GetAltitude() -local CurrentPointVec3=POINT_VEC3:New(CurrentVec2.x,CurrentAltitude,CurrentVec2.y) -local ToEngageZoneSpeed=self.PatrolMaxSpeed -local CurrentRoutePoint=CurrentPointVec3:WaypointAir( -self.PatrolAltType, -POINT_VEC3.RoutePointType.TurningPoint, -POINT_VEC3.RoutePointAction.TurningPoint, -self.EngageSpeed, -true -) -EngageRoute[#EngageRoute+1]=CurrentRoutePoint -local AttackTasks={} -for DetectedUnit,Detected in pairs(self.DetectedUnits)do -local DetectedUnit=DetectedUnit -self:T(DetectedUnit) -if DetectedUnit:IsAlive()then -if DetectedUnit:IsInZone(self.EngageZone)then -self:E({"Engaging ",DetectedUnit}) -AttackTasks[#AttackTasks+1]=Controllable:TaskAttackUnit(DetectedUnit, -true, -EngageWeaponExpend, -EngageAttackQty, -EngageDirection -) -end -else -self.DetectedUnits[DetectedUnit]=nil -end -end -AttackTasks[#AttackTasks+1]=Controllable:TaskFunction("AI_CAS_ZONE.EngageRoute",self) -EngageRoute[#EngageRoute].task=Controllable:TaskCombo(AttackTasks) -local ToTargetVec2=self.EngageZone:GetRandomVec2() -self:T2(ToTargetVec2) -local ToTargetPointVec3=POINT_VEC3:New(ToTargetVec2.x,self.EngageAltitude,ToTargetVec2.y) -local ToTargetRoutePoint=ToTargetPointVec3:WaypointAir( -self.PatrolAltType, -POINT_VEC3.RoutePointType.TurningPoint, -POINT_VEC3.RoutePointAction.TurningPoint, -self.EngageSpeed, -true -) -EngageRoute[#EngageRoute+1]=ToTargetRoutePoint -Controllable:Route(EngageRoute,0.5) -self:SetRefreshTimeInterval(2) -self:SetDetectionActivated() -self:__Target(-2) -end -end -function AI_CAS_ZONE:onafterAccomplish(Controllable,From,Event,To) -self.Accomplished=true -self:SetDetectionDeactivated() -end -function AI_CAS_ZONE:onafterDestroy(Controllable,From,Event,To,EventData) -if EventData.IniUnit then -self.DetectedUnits[EventData.IniUnit]=nil -end -end -function AI_CAS_ZONE:OnEventDead(EventData) -self:F({"EventDead",EventData}) -if EventData.IniDCSUnit then -if self.DetectedUnits and self.DetectedUnits[EventData.IniUnit]then -self:__Destroy(1,EventData) -end -end -end -AI_BAI_ZONE={ -ClassName="AI_BAI_ZONE", -} -function AI_BAI_ZONE:New(PatrolZone,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,EngageZone,PatrolAltType) -local self=BASE:Inherit(self,AI_PATROL_ZONE:New(PatrolZone,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,PatrolAltType)) -self.EngageZone=EngageZone -self.Accomplished=false -self:SetDetectionZone(self.EngageZone) -self:SearchOn() -self:AddTransition({"Patrolling","Engaging"},"Engage","Engaging") -self:AddTransition("Engaging","Target","Engaging") -self:AddTransition("Engaging","Fired","Engaging") -self:AddTransition("*","Destroy","*") -self:AddTransition("Engaging","Abort","Patrolling") -self:AddTransition("Engaging","Accomplish","Patrolling") -return self -end -function AI_BAI_ZONE:SetEngageZone(EngageZone) -self:F2() -if EngageZone then -self.EngageZone=EngageZone -else -self.EngageZone=nil -end -end -function AI_BAI_ZONE:SearchOnOff(Search) -self.Search=Search -return self -end -function AI_BAI_ZONE:SearchOff() -self:SearchOnOff(false) -return self -end -function AI_BAI_ZONE:SearchOn() -self:SearchOnOff(true) -return self -end -function AI_BAI_ZONE:onafterStart(Controllable,From,Event,To) -self:GetParent(self).onafterStart(self,Controllable,From,Event,To) -self:HandleEvent(EVENTS.Dead) -self:SetDetectionDeactivated() -end -function _NewEngageRoute(AIControllable) -AIControllable:T("NewEngageRoute") -local EngageZone=AIControllable:GetState(AIControllable,"EngageZone") -EngageZone:__Engage(1,EngageZone.EngageSpeed,EngageZone.EngageAltitude,EngageZone.EngageWeaponExpend,EngageZone.EngageAttackQty,EngageZone.EngageDirection) -end -function AI_BAI_ZONE:onbeforeEngage(Controllable,From,Event,To) -if self.Accomplished==true then -return false -end -end -function AI_BAI_ZONE:onafterTarget(Controllable,From,Event,To) -self:F({"onafterTarget",self.Search,Controllable:IsAlive()}) -if Controllable:IsAlive()then -local AttackTasks={} -if self.Search==true then -for DetectedUnit,Detected in pairs(self.DetectedUnits)do -local DetectedUnit=DetectedUnit -if DetectedUnit:IsAlive()then -if DetectedUnit:IsInZone(self.EngageZone)then -if Detected==true then -self:F({"Target: ",DetectedUnit}) -self.DetectedUnits[DetectedUnit]=false -local AttackTask=Controllable:TaskAttackUnit(DetectedUnit,false,self.EngageWeaponExpend,self.EngageAttackQty,self.EngageDirection,self.EngageAltitude,nil) -self.Controllable:PushTask(AttackTask,1) -end -end -else -self.DetectedUnits[DetectedUnit]=nil -end -end -else -self:F("Attack zone") -local AttackTask=Controllable:TaskAttackMapObject( -self.EngageZone:GetPointVec2():GetVec2(), -true, -self.EngageWeaponExpend, -self.EngageAttackQty, -self.EngageDirection, -self.EngageAltitude -) -self.Controllable:PushTask(AttackTask,1) -end -self:__Target(-10) -end -end -function AI_BAI_ZONE:onafterAbort(Controllable,From,Event,To) -Controllable:ClearTasks() -self:__Route(1) -end -function AI_BAI_ZONE:onafterEngage(Controllable,From,Event,To, -EngageSpeed, -EngageAltitude, -EngageWeaponExpend, -EngageAttackQty, -EngageDirection) -self:F("onafterEngage") -self.EngageSpeed=EngageSpeed or 400 -self.EngageAltitude=EngageAltitude or 2000 -self.EngageWeaponExpend=EngageWeaponExpend -self.EngageAttackQty=EngageAttackQty -self.EngageDirection=EngageDirection -if Controllable:IsAlive()then -local EngageRoute={} -local CurrentVec2=self.Controllable:GetVec2() -local CurrentAltitude=self.Controllable:GetUnit(1):GetAltitude() -local CurrentPointVec3=POINT_VEC3:New(CurrentVec2.x,CurrentAltitude,CurrentVec2.y) -local ToEngageZoneSpeed=self.PatrolMaxSpeed -local CurrentRoutePoint=CurrentPointVec3:WaypointAir( -self.PatrolAltType, -POINT_VEC3.RoutePointType.TurningPoint, -POINT_VEC3.RoutePointAction.TurningPoint, -self.EngageSpeed, -true -) -EngageRoute[#EngageRoute+1]=CurrentRoutePoint -local AttackTasks={} -if self.Search==true then -for DetectedUnitID,DetectedUnitData in pairs(self.DetectedUnits)do -local DetectedUnit=DetectedUnitData -self:T(DetectedUnit) -if DetectedUnit:IsAlive()then -if DetectedUnit:IsInZone(self.EngageZone)then -self:F({"Engaging ",DetectedUnit}) -AttackTasks[#AttackTasks+1]=Controllable:TaskBombing( -DetectedUnit:GetPointVec2():GetVec2(), -true, -EngageWeaponExpend, -EngageAttackQty, -EngageDirection, -EngageAltitude -) -end -else -self.DetectedUnits[DetectedUnit]=nil -end -end -else -self:F("Attack zone") -AttackTasks[#AttackTasks+1]=Controllable:TaskAttackMapObject( -self.EngageZone:GetPointVec2():GetVec2(), -true, -EngageWeaponExpend, -EngageAttackQty, -EngageDirection, -EngageAltitude -) -end -EngageRoute[#EngageRoute].task=Controllable:TaskCombo(AttackTasks) -local ToTargetVec2=self.EngageZone:GetRandomVec2() -self:T2(ToTargetVec2) -local ToTargetPointVec3=POINT_VEC3:New(ToTargetVec2.x,self.EngageAltitude,ToTargetVec2.y) -local ToTargetRoutePoint=ToTargetPointVec3:WaypointAir( -self.PatrolAltType, -POINT_VEC3.RoutePointType.TurningPoint, -POINT_VEC3.RoutePointAction.TurningPoint, -self.EngageSpeed, -true -) -EngageRoute[#EngageRoute+1]=ToTargetRoutePoint -Controllable:OptionROEOpenFire() -Controllable:OptionROTVertical() -Controllable:WayPointInitialize(EngageRoute) -Controllable:SetState(Controllable,"EngageZone",self) -Controllable:WayPointFunction(#EngageRoute,1,"_NewEngageRoute") -Controllable:WayPointExecute(1) -self:SetRefreshTimeInterval(2) -self:SetDetectionActivated() -self:__Target(-2) -end -end -function AI_BAI_ZONE:onafterAccomplish(Controllable,From,Event,To) -self.Accomplished=true -self:SetDetectionDeactivated() -end -function AI_BAI_ZONE:onafterDestroy(Controllable,From,Event,To,EventData) -if EventData.IniUnit then -self.DetectedUnits[EventData.IniUnit]=nil -end -end -function AI_BAI_ZONE:OnEventDead(EventData) -self:F({"EventDead",EventData}) -if EventData.IniDCSUnit then -if self.DetectedUnits and self.DetectedUnits[EventData.IniUnit]then -self:__Destroy(1,EventData) -end -end -end -AI_FORMATION={ -ClassName="AI_FORMATION", -FollowName=nil, -FollowUnit=nil, -FollowGroupSet=nil, -FollowMode=1, -MODE={ -FOLLOW=1, -MISSION=2, -}, -FollowScheduler=nil, -OptionROE=AI.Option.Air.val.ROE.OPEN_FIRE, -OptionReactionOnThreat=AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION, -} -function AI_FORMATION:New(FollowUnit,FollowGroupSet,FollowName,FollowBriefing) -local self=BASE:Inherit(self,FSM_SET:New(FollowGroupSet)) -self:F({FollowUnit,FollowGroupSet,FollowName}) -self.FollowUnit=FollowUnit -self.FollowGroupSet=FollowGroupSet -self:SetFlightRandomization(2) -self:SetStartState("None") -self:AddTransition("*","Stop","Stopped") -self:AddTransition("None","Start","Following") -self:AddTransition("*","FormationLine","*") -self:AddTransition("*","FormationTrail","*") -self:AddTransition("*","FormationStack","*") -self:AddTransition("*","FormationLeftLine","*") -self:AddTransition("*","FormationRightLine","*") -self:AddTransition("*","FormationLeftWing","*") -self:AddTransition("*","FormationRightWing","*") -self:AddTransition("*","FormationCenterWing","*") -self:AddTransition("*","FormationVic","*") -self:AddTransition("*","FormationBox","*") -self:AddTransition("*","Follow","Following") -self:FormationLeftLine(500,0,250,250) -self.FollowName=FollowName -self.FollowBriefing=FollowBriefing -self.CT1=0 -self.GT1=0 -self.FollowMode=AI_FORMATION.MODE.MISSION -return self -end -function AI_FORMATION:TestSmokeDirectionVector(SmokeDirection) -self.SmokeDirectionVector=(SmokeDirection==true)and true or false -return self -end -function AI_FORMATION:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,ZStart,ZSpace) -self:F({FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,ZStart,ZSpace}) -FollowGroupSet:Flush() -local FollowSet=FollowGroupSet:GetSet() -local i=0 -for FollowID,FollowGroup in pairs(FollowSet)do -local PointVec3=POINT_VEC3:New() -PointVec3:SetX(XStart+i*XSpace) -PointVec3:SetY(YStart+i*YSpace) -PointVec3:SetZ(ZStart+i*ZSpace) -local Vec3=PointVec3:GetVec3() -FollowGroup:SetState(self,"FormationVec3",Vec3) -i=i+1 -end -return self -end -function AI_FORMATION:onafterFormationTrail(FollowGroupSet,From,Event,To,XStart,XSpace,YStart) -self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,0,0) -return self -end -function AI_FORMATION:onafterFormationStack(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace) -self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,0,0) -return self -end -function AI_FORMATION:onafterFormationLeftLine(FollowGroupSet,From,Event,To,XStart,YStart,ZStart,ZSpace) -self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,ZStart,ZSpace) -return self -end -function AI_FORMATION:onafterFormationRightLine(FollowGroupSet,From,Event,To,XStart,YStart,ZStart,ZSpace) -self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,-ZStart,-ZSpace) -return self -end -function AI_FORMATION:onafterFormationLeftWing(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,ZStart,ZSpace) -self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,ZStart,ZSpace) -return self -end -function AI_FORMATION:onafterFormationRightWing(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,ZStart,ZSpace) -self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,-ZStart,-ZSpace) -return self -end -function AI_FORMATION:onafterFormationCenterWing(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,ZStart,ZSpace) -local FollowSet=FollowGroupSet:GetSet() -local i=0 -for FollowID,FollowGroup in pairs(FollowSet)do -local PointVec3=POINT_VEC3:New() -local Side=(i%2==0)and 1 or-1 -local Row=i/2+1 -PointVec3:SetX(XStart+Row*XSpace) -PointVec3:SetY(YStart) -PointVec3:SetZ(Side*(ZStart+i*ZSpace)) -local Vec3=PointVec3:GetVec3() -FollowGroup:SetState(self,"FormationVec3",Vec3) -i=i+1 -end -return self -end -function AI_FORMATION:onafterFormationVic(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,ZStart,ZSpace) -self:onafterFormationCenterWing(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,ZStart,ZSpace) -return self -end -function AI_FORMATION:onafterFormationBox(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,ZStart,ZSpace,ZLevels) -local FollowSet=FollowGroupSet:GetSet() -local i=0 -for FollowID,FollowGroup in pairs(FollowSet)do -local PointVec3=POINT_VEC3:New() -local ZIndex=i%ZLevels -local XIndex=math.floor(i/ZLevels) -local YIndex=math.floor(i/ZLevels) -PointVec3:SetX(XStart+XIndex*XSpace) -PointVec3:SetY(YStart+YIndex*YSpace) -PointVec3:SetZ(-ZStart-(ZSpace*ZLevels/2)+ZSpace*ZIndex) -local Vec3=PointVec3:GetVec3() -FollowGroup:SetState(self,"FormationVec3",Vec3) -i=i+1 -end -return self -end -function AI_FORMATION:SetFlightRandomization(FlightRandomization) -self.FlightRandomization=FlightRandomization -return self -end -function AI_FORMATION:onenterFollowing(FollowGroupSet) -self:F() -self:T({self.FollowUnit.UnitName,self.FollowUnit:IsAlive()}) -if self.FollowUnit:IsAlive()then -local ClientUnit=self.FollowUnit -self:T({ClientUnit.UnitName}) -local CT1,CT2,CV1,CV2 -CT1=ClientUnit:GetState(self,"CT1") -if CT1==nil or CT1==0 then -ClientUnit:SetState(self,"CV1",ClientUnit:GetPointVec3()) -ClientUnit:SetState(self,"CT1",timer.getTime()) -else -CT1=ClientUnit:GetState(self,"CT1") -CT2=timer.getTime() -CV1=ClientUnit:GetState(self,"CV1") -CV2=ClientUnit:GetPointVec3() -ClientUnit:SetState(self,"CT1",CT2) -ClientUnit:SetState(self,"CV1",CV2) -end -FollowGroupSet:ForEachGroup( -function(FollowGroup,Formation,ClientUnit,CT1,CV1,CT2,CV2) -FollowGroup:OptionROTEvadeFire() -FollowGroup:OptionROEReturnFire() -local GroupUnit=FollowGroup:GetUnit(1) -local FollowFormation=FollowGroup:GetState(self,"FormationVec3") -if FollowFormation then -local FollowDistance=FollowFormation.x -local GT1=GroupUnit:GetState(self,"GT1") -if CT1==nil or CT1==0 or GT1==nil or GT1==0 then -GroupUnit:SetState(self,"GV1",GroupUnit:GetPointVec3()) -GroupUnit:SetState(self,"GT1",timer.getTime()) -else -local CD=((CV2.x-CV1.x)^2+(CV2.y-CV1.y)^2+(CV2.z-CV1.z)^2)^0.5 -local CT=CT2-CT1 -local CS=(3600/CT)*(CD/1000)/3.6 -local CDv={x=CV2.x-CV1.x,y=CV2.y-CV1.y,z=CV2.z-CV1.z} -local Ca=math.atan2(CDv.x,CDv.z) -local GT1=GroupUnit:GetState(self,"GT1") -local GT2=timer.getTime() -local GV1=GroupUnit:GetState(self,"GV1") -local GV2=GroupUnit:GetPointVec3() -GV2:AddX(math.random(-Formation.FlightRandomization/2,Formation.FlightRandomization/2)) -GV2:AddY(math.random(-Formation.FlightRandomization/2,Formation.FlightRandomization/2)) -GV2:AddZ(math.random(-Formation.FlightRandomization/2,Formation.FlightRandomization/2)) -GroupUnit:SetState(self,"GT1",GT2) -GroupUnit:SetState(self,"GV1",GV2) -local GD=((GV2.x-GV1.x)^2+(GV2.y-GV1.y)^2+(GV2.z-GV1.z)^2)^0.5 -local GT=GT2-GT1 -local GDv={x=GV2.x-CV1.x,y=GV2.y-CV1.y,z=GV2.z-CV1.z} -local Alpha_T=math.atan2(GDv.x,GDv.z)-math.atan2(CDv.x,CDv.z) -local Alpha_R=(Alpha_T<0)and Alpha_T+2*math.pi or Alpha_T -local Position=math.cos(Alpha_R) -local GD=((GDv.x)^2+(GDv.z)^2)^0.5 -local Distance=GD*Position+-CS*0,5 -local GV={x=GV2.x-CV2.x,y=GV2.y-CV2.y,z=GV2.z-CV2.z} -local GH2={x=GV2.x,y=CV2.y+FollowFormation.y,z=GV2.z} -local alpha=math.atan2(GV.x,GV.z) -local GVx=FollowFormation.z*math.cos(Ca)+FollowFormation.x*math.sin(Ca) -local GVz=FollowFormation.x*math.cos(Ca)-FollowFormation.z*math.sin(Ca) -local CVI={x=CV2.x+CS*10*math.sin(Ca), -y=GH2.y-(Distance+FollowFormation.x)/5, -z=CV2.z+CS*10*math.cos(Ca), -} -local DV={x=CV2.x-CVI.x,y=CV2.y-CVI.y,z=CV2.z-CVI.z} -local DVu={x=DV.x/FollowDistance,y=DV.y,z=DV.z/FollowDistance} -local GDV={x=CVI.x,y=CVI.y,z=CVI.z} -local ADDx=FollowFormation.x*math.cos(alpha)-FollowFormation.z*math.sin(alpha) -local ADDz=FollowFormation.z*math.cos(alpha)+FollowFormation.x*math.sin(alpha) -local GDV_Formation={ -x=GDV.x-GVx, -y=GDV.y, -z=GDV.z-GVz -} -if self.SmokeDirectionVector==true then -trigger.action.smoke(GDV,trigger.smokeColor.Green) -trigger.action.smoke(GDV_Formation,trigger.smokeColor.White) -end -local Time=60 -local Speed=-(Distance+FollowFormation.x)/Time -local GS=Speed+CS -if Speed<0 then -Speed=0 -end -FollowGroup:RouteToVec3(GDV_Formation,GS) -end -end -end, -self,ClientUnit,CT1,CV1,CT2,CV2 -) -self:__Follow(-0.5) -end -end -do -ACT_ASSIGN={ -ClassName="ACT_ASSIGN", -} -function ACT_ASSIGN:New() -local self=BASE:Inherit(self,FSM_PROCESS:New("ACT_ASSIGN")) -self:AddTransition("UnAssigned","Start","Waiting") -self:AddTransition("Waiting","Assign","Assigned") -self:AddTransition("Waiting","Reject","Rejected") -self:AddTransition("*","Fail","Failed") -self:AddEndState("Assigned") -self:AddEndState("Rejected") -self:AddEndState("Failed") -self:SetStartState("UnAssigned") -return self -end -end -do -ACT_ASSIGN_ACCEPT={ -ClassName="ACT_ASSIGN_ACCEPT", -} -function ACT_ASSIGN_ACCEPT:New(TaskBriefing) -local self=BASE:Inherit(self,ACT_ASSIGN:New()) -self.TaskBriefing=TaskBriefing -return self -end -function ACT_ASSIGN_ACCEPT:Init(FsmAssign) -self.TaskBriefing=FsmAssign.TaskBriefing -end -function ACT_ASSIGN_ACCEPT:onafterStart(ProcessUnit,From,Event,To) -self:E({ProcessUnit,From,Event,To}) -self:__Assign(1) -end -function ACT_ASSIGN_ACCEPT:onenterAssigned(ProcessUnit,From,Event,To) -env.info("in here") -self:E({ProcessUnit,From,Event,To}) -local ProcessGroup=ProcessUnit:GetGroup() -self.Task:Assign(ProcessUnit,ProcessUnit:GetPlayerName()) -end -end -do -ACT_ASSIGN_MENU_ACCEPT={ -ClassName="ACT_ASSIGN_MENU_ACCEPT", -} -function ACT_ASSIGN_MENU_ACCEPT:New(TaskName,TaskBriefing) -local self=BASE:Inherit(self,ACT_ASSIGN:New()) -self.TaskName=TaskName -self.TaskBriefing=TaskBriefing -return self -end -function ACT_ASSIGN_MENU_ACCEPT:Init(FsmAssign) -self.TaskName=FsmAssign.TaskName -self.TaskBriefing=FsmAssign.TaskBriefing -end -function ACT_ASSIGN_MENU_ACCEPT:Init(TaskName,TaskBriefing) -self.TaskBriefing=TaskBriefing -self.TaskName=TaskName -return self -end -function ACT_ASSIGN_MENU_ACCEPT:onafterStart(ProcessUnit,From,Event,To) -self:E({ProcessUnit,From,Event,To}) -self:GetCommandCenter():MessageTypeToGroup("Access the radio menu to accept the task. You have 30 seconds or the assignment will be cancelled.",ProcessUnit:GetGroup(),MESSAGE.Type.Information) -local ProcessGroup=ProcessUnit:GetGroup() -self.Menu=MENU_GROUP:New(ProcessGroup,"Task "..self.TaskName.." acceptance") -self.MenuAcceptTask=MENU_GROUP_COMMAND:New(ProcessGroup,"Accept task "..self.TaskName,self.Menu,self.MenuAssign,self) -self.MenuRejectTask=MENU_GROUP_COMMAND:New(ProcessGroup,"Reject task "..self.TaskName,self.Menu,self.MenuReject,self) -end -function ACT_ASSIGN_MENU_ACCEPT:MenuAssign() -self:E() -self:__Assign(1) -end -function ACT_ASSIGN_MENU_ACCEPT:MenuReject() -self:E() -self:__Reject(1) -end -function ACT_ASSIGN_MENU_ACCEPT:onafterAssign(ProcessUnit,From,Event,To) -self:E({ProcessUnit.UnitNameFrom,Event,To}) -self.Menu:Remove() -end -function ACT_ASSIGN_MENU_ACCEPT:onafterReject(ProcessUnit,From,Event,To) -self:E({ProcessUnit.UnitName,From,Event,To}) -self.Menu:Remove() -ProcessUnit:Destroy() -end -end -do -ACT_ROUTE={ -ClassName="ACT_ROUTE", -} -function ACT_ROUTE:New() -local self=BASE:Inherit(self,FSM_PROCESS:New("ACT_ROUTE")) -self:AddTransition("*","Reset","None") -self:AddTransition("None","Start","Routing") -self:AddTransition("*","Report","*") -self:AddTransition("Routing","Route","Routing") -self:AddTransition("Routing","Pause","Pausing") -self:AddTransition("Routing","Arrive","Arrived") -self:AddTransition("*","Cancel","Cancelled") -self:AddTransition("Arrived","Success","Success") -self:AddTransition("*","Fail","Failed") -self:AddTransition("","","") -self:AddTransition("","","") -self:AddEndState("Arrived") -self:AddEndState("Failed") -self:AddEndState("Cancelled") -self:SetStartState("None") -self:SetRouteMode("C") -return self -end -function ACT_ROUTE:SetMenuCancel(MenuGroup,MenuText,ParentMenu,MenuTime) -MENU_GROUP_COMMAND:New( -MenuGroup, -MenuText, -ParentMenu, -self.MenuCancel, -self -):SetTime(MenuTime) -return self -end -function ACT_ROUTE:SetRouteMode(RouteMode) -self.RouteMode=RouteMode -return self -end -function ACT_ROUTE:GetRouteText(Controllable) -self:E() -local RouteText="" -local Coordinate=nil -if self.Coordinate then -Coordinate=self.Coordinate -end -if self.Zone then -Coordinate=self.Zone:GetPointVec3(self.Altitude) -Coordinate:SetHeading(self.Heading) -end -local Task=self:GetTask() -local CC=self:GetTask():GetMission():GetCommandCenter() -if CC then -if CC:IsModeWWII()then -local ShortestDistance=0 -local ShortestReferencePoint=nil -local ShortestReferenceName="" -self:E({CC.ReferencePoints}) -for ZoneName,Zone in pairs(CC.ReferencePoints)do -self:E({ZoneName=ZoneName}) -local Zone=Zone -local ZoneCoord=Zone:GetCoordinate() -local ZoneDistance=ZoneCoord:Get2DDistance(self.Coordinate) -self:E({ShortestDistance,ShortestReferenceName}) -if ShortestDistance==0 or ZoneDistance=self.DisplayInterval then -self:T({HasArrived=HasArrived}) -if not HasArrived then -self:Report() -end -self.DisplayCount=1 -else -self.DisplayCount=self.DisplayCount+1 -end -self:T({DisplayCount=self.DisplayCount}) -if HasArrived then -self:__Arrive(1) -else -self:__Route(1) -end -return HasArrived -end -return false -end -end -do -ACT_ROUTE_POINT={ -ClassName="ACT_ROUTE_POINT", -} -function ACT_ROUTE_POINT:New(Coordinate,Range) -local self=BASE:Inherit(self,ACT_ROUTE:New()) -self.Coordinate=Coordinate -self.Range=Range or 0 -self.DisplayInterval=30 -self.DisplayCount=30 -self.DisplayMessage=true -self.DisplayTime=10 -return self -end -function ACT_ROUTE_POINT:Init(FsmRoute) -self.Coordinate=FsmRoute.Coordinate -self.Range=FsmRoute.Range or 0 -self.DisplayInterval=30 -self.DisplayCount=30 -self.DisplayMessage=true -self.DisplayTime=10 -self:SetStartState("None") -end -function ACT_ROUTE_POINT:SetCoordinate(Coordinate) -self:F2({Coordinate}) -self.Coordinate=Coordinate -end -function ACT_ROUTE_POINT:GetCoordinate() -self:F2({self.Coordinate}) -return self.Coordinate -end -function ACT_ROUTE_POINT:SetRange(Range) -self:F2({self.Range}) -self.Range=Range or 10000 -end -function ACT_ROUTE_POINT:GetRange() -return self.Range -end -function ACT_ROUTE_POINT:onfuncHasArrived(ProcessUnit) -if ProcessUnit:IsAlive()then -local Distance=self.Coordinate:Get2DDistance(ProcessUnit:GetCoordinate()) -if Distance<=self.Range then -local RouteText="You have arrived." -self:GetCommandCenter():MessageTypeToGroup(RouteText,ProcessUnit:GetGroup(),MESSAGE.Type.Information) -return true -end -end -return false -end -function ACT_ROUTE_POINT:onafterReport(ProcessUnit,From,Event,To) -local RouteText=self:GetRouteText(ProcessUnit) -self:GetCommandCenter():MessageTypeToGroup(RouteText,ProcessUnit:GetGroup(),MESSAGE.Type.Update) -end -end -do -ACT_ROUTE_ZONE={ -ClassName="ACT_ROUTE_ZONE", -} -function ACT_ROUTE_ZONE:New(Zone) -local self=BASE:Inherit(self,ACT_ROUTE:New()) -self.Zone=Zone -self.DisplayInterval=30 -self.DisplayCount=30 -self.DisplayMessage=true -self.DisplayTime=10 -return self -end -function ACT_ROUTE_ZONE:Init(FsmRoute) -self.Zone=FsmRoute.Zone -self.DisplayInterval=30 -self.DisplayCount=30 -self.DisplayMessage=true -self.DisplayTime=10 -end -function ACT_ROUTE_ZONE:SetZone(Zone,Altitude,Heading) -self.Zone=Zone -self.Altitude=Altitude -self.Heading=Heading -end -function ACT_ROUTE_ZONE:GetZone() -return self.Zone -end -function ACT_ROUTE_ZONE:onfuncHasArrived(ProcessUnit) -if ProcessUnit:IsInZone(self.Zone)then -local RouteText="You have arrived within the zone." -self:GetCommandCenter():MessageTypeToGroup(RouteText,ProcessUnit:GetGroup(),MESSAGE.Type.Information) -end -return ProcessUnit:IsInZone(self.Zone) -end -function ACT_ROUTE_ZONE:onafterReport(ProcessUnit,From,Event,To) -self:E({ProcessUnit=ProcessUnit}) -local RouteText=self:GetRouteText(ProcessUnit) -self:GetCommandCenter():MessageTypeToGroup(RouteText,ProcessUnit:GetGroup(),MESSAGE.Type.Update) -end -end -do -ACT_ACCOUNT={ -ClassName="ACT_ACCOUNT", -TargetSetUnit=nil, -} -function ACT_ACCOUNT:New() -local self=BASE:Inherit(self,FSM_PROCESS:New()) -self:AddTransition("Assigned","Start","Waiting") -self:AddTransition("*","Wait","Waiting") -self:AddTransition("*","Report","Report") -self:AddTransition("*","Event","Account") -self:AddTransition("Account","Player","AccountForPlayer") -self:AddTransition("Account","Other","AccountForOther") -self:AddTransition({"Account","AccountForPlayer","AccountForOther"},"More","Wait") -self:AddTransition({"Account","AccountForPlayer","AccountForOther"},"NoMore","Accounted") -self:AddTransition("*","Fail","Failed") -self:AddEndState("Failed") -self:SetStartState("Assigned") -return self -end -function ACT_ACCOUNT:onafterStart(ProcessUnit,From,Event,To) -self:HandleEvent(EVENTS.Dead,self.onfuncEventDead) -self:HandleEvent(EVENTS.Crash,self.onfuncEventCrash) -self:HandleEvent(EVENTS.Hit) -self:__Wait(1) -end -function ACT_ACCOUNT:onenterWaiting(ProcessUnit,From,Event,To) -if self.DisplayCount>=self.DisplayInterval then -self:Report() -self.DisplayCount=1 -else -self.DisplayCount=self.DisplayCount+1 -end -return true -end -function ACT_ACCOUNT:onafterEvent(ProcessUnit,From,Event,To,Event) -self:__NoMore(1) -end -end -do -ACT_ACCOUNT_DEADS={ -ClassName="ACT_ACCOUNT_DEADS", -} -function ACT_ACCOUNT_DEADS:New() -local self=BASE:Inherit(self,ACT_ACCOUNT:New()) -self.DisplayInterval=30 -self.DisplayCount=30 -self.DisplayMessage=true -self.DisplayTime=10 -self.DisplayCategory="HQ" -return self -end -function ACT_ACCOUNT_DEADS:Init(FsmAccount) -self.Task=self:GetTask() -self.TaskName=self.Task:GetName() -end -function ACT_ACCOUNT_DEADS:onenterReport(ProcessUnit,Task,From,Event,To) -self:E({ProcessUnit,From,Event,To}) -local MessageText="Your group with assigned "..self.TaskName.." task has "..Task.TargetSetUnit:GetUnitTypesText().." targets left to be destroyed." -self:GetCommandCenter():MessageTypeToGroup(MessageText,ProcessUnit:GetGroup(),MESSAGE.Type.Information) -end -function ACT_ACCOUNT_DEADS:onafterEvent(ProcessUnit,Task,From,Event,To,EventData) -self:T({ProcessUnit:GetName(),Task:GetName(),From,Event,To,EventData}) -if Task.TargetSetUnit:FindUnit(EventData.IniUnitName)then -local PlayerName=ProcessUnit:GetPlayerName() -local PlayerHit=self.PlayerHits and self.PlayerHits[EventData.IniUnitName] -if PlayerHit==PlayerName then -self:Player(EventData) -else -self:Other(EventData) -end -end -end -function ACT_ACCOUNT_DEADS:onenterAccountForPlayer(ProcessUnit,Task,From,Event,To,EventData) -self:T({ProcessUnit:GetName(),Task:GetName(),From,Event,To,EventData}) -local TaskGroup=ProcessUnit:GetGroup() -Task.TargetSetUnit:Remove(EventData.IniUnitName) -local MessageText="You have destroyed a target.\nYour group assigned with task "..self.TaskName.." has\n"..Task.TargetSetUnit:Count().." targets ( "..Task.TargetSetUnit:GetUnitTypesText().." ) left to be destroyed." -self:GetCommandCenter():MessageTypeToGroup(MessageText,ProcessUnit:GetGroup(),MESSAGE.Type.Information) -local PlayerName=ProcessUnit:GetPlayerName() -Task:AddProgress(PlayerName,"Destroyed "..EventData.IniTypeName,timer.getTime(),1) -if Task.TargetSetUnit:Count()>0 then -self:__More(1) -else -self:__NoMore(1) -end -end -function ACT_ACCOUNT_DEADS:onenterAccountForOther(ProcessUnit,Task,From,Event,To,EventData) -self:T({ProcessUnit:GetName(),Task:GetName(),From,Event,To,EventData}) -local TaskGroup=ProcessUnit:GetGroup() -Task.TargetSetUnit:Remove(EventData.IniUnitName) -local MessageText="One of the task targets has been destroyed.\nYour group assigned with task "..self.TaskName.." has\n"..Task.TargetSetUnit:Count().." targets ( "..Task.TargetSetUnit:GetUnitTypesText().." ) left to be destroyed." -self:GetCommandCenter():MessageTypeToGroup(MessageText,ProcessUnit:GetGroup(),MESSAGE.Type.Information) -if Task.TargetSetUnit:Count()>0 then -self:__More(1) -else -self:__NoMore(1) -end -end -function ACT_ACCOUNT_DEADS:OnEventHit(EventData) -self:T({"EventDead",EventData}) -if EventData.IniPlayerName and EventData.TgtDCSUnitName then -self.PlayerHits=self.PlayerHits or{} -self.PlayerHits[EventData.TgtDCSUnitName]=EventData.IniPlayerName -end -end -function ACT_ACCOUNT_DEADS:onfuncEventDead(EventData) -self:T({"EventDead",EventData}) -if EventData.IniDCSUnit then -self:Event(EventData) -end -end -function ACT_ACCOUNT_DEADS:onfuncEventCrash(EventData) -self:T({"EventDead",EventData}) -if EventData.IniDCSUnit then -self:Event(EventData) -end -end -end -do -ACT_ASSIST={ -ClassName="ACT_ASSIST", -} -function ACT_ASSIST:New() -local self=BASE:Inherit(self,FSM_PROCESS:New("ACT_ASSIST")) -self:AddTransition("None","Start","AwaitSmoke") -self:AddTransition("AwaitSmoke","Next","Smoking") -self:AddTransition("Smoking","Next","AwaitSmoke") -self:AddTransition("*","Stop","Success") -self:AddTransition("*","Fail","Failed") -self:AddEndState("Failed") -self:AddEndState("Success") -self:SetStartState("None") -return self -end -function ACT_ASSIST:onafterStart(ProcessUnit,From,Event,To) -local ProcessGroup=ProcessUnit:GetGroup() -local MissionMenu=self:GetMission():GetMenu(ProcessGroup) -local function MenuSmoke(MenuParam) -self:E(MenuParam) -local self=MenuParam.self -local SmokeColor=MenuParam.SmokeColor -self.SmokeColor=SmokeColor -self:__Next(1) -end -self.Menu=MENU_GROUP:New(ProcessGroup,"Target acquisition",MissionMenu) -self.MenuSmokeBlue=MENU_GROUP_COMMAND:New(ProcessGroup,"Drop blue smoke on targets",self.Menu,MenuSmoke,{self=self,SmokeColor=SMOKECOLOR.Blue}) -self.MenuSmokeGreen=MENU_GROUP_COMMAND:New(ProcessGroup,"Drop green smoke on targets",self.Menu,MenuSmoke,{self=self,SmokeColor=SMOKECOLOR.Green}) -self.MenuSmokeOrange=MENU_GROUP_COMMAND:New(ProcessGroup,"Drop Orange smoke on targets",self.Menu,MenuSmoke,{self=self,SmokeColor=SMOKECOLOR.Orange}) -self.MenuSmokeRed=MENU_GROUP_COMMAND:New(ProcessGroup,"Drop Red smoke on targets",self.Menu,MenuSmoke,{self=self,SmokeColor=SMOKECOLOR.Red}) -self.MenuSmokeWhite=MENU_GROUP_COMMAND:New(ProcessGroup,"Drop White smoke on targets",self.Menu,MenuSmoke,{self=self,SmokeColor=SMOKECOLOR.White}) -end -function ACT_ASSIST:onafterStop(ProcessUnit,From,Event,To) -self.Menu:Remove() -end -end -do -ACT_ASSIST_SMOKE_TARGETS_ZONE={ -ClassName="ACT_ASSIST_SMOKE_TARGETS_ZONE", -} -function ACT_ASSIST_SMOKE_TARGETS_ZONE:New(TargetSetUnit,TargetZone) -local self=BASE:Inherit(self,ACT_ASSIST:New()) -self.TargetSetUnit=TargetSetUnit -self.TargetZone=TargetZone -return self -end -function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init(FsmSmoke) -self.TargetSetUnit=FsmSmoke.TargetSetUnit -self.TargetZone=FsmSmoke.TargetZone -end -function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init(TargetSetUnit,TargetZone) -self.TargetSetUnit=TargetSetUnit -self.TargetZone=TargetZone -return self -end -function ACT_ASSIST_SMOKE_TARGETS_ZONE:onenterSmoking(ProcessUnit,From,Event,To) -self.TargetSetUnit:ForEachUnit( -function(SmokeUnit) -if math.random(1,(100*self.TargetSetUnit:Count())/4)<=100 then -SCHEDULER:New(self, -function() -if SmokeUnit:IsAlive()then -SmokeUnit:Smoke(self.SmokeColor,150) -end -end,{},math.random(10,60) -) -end -end -) -end -end -COMMANDCENTER={ -ClassName="COMMANDCENTER", -CommandCenterName="", -CommandCenterCoalition=nil, -CommandCenterPositionable=nil, -Name="", -ReferencePoints={}, -ReferenceNames={}, -CommunicationMode="80", -} -function COMMANDCENTER:New(CommandCenterPositionable,CommandCenterName) -local self=BASE:Inherit(self,BASE:New()) -self.CommandCenterPositionable=CommandCenterPositionable -self.CommandCenterName=CommandCenterName or CommandCenterPositionable:GetName() -self.CommandCenterCoalition=CommandCenterPositionable:GetCoalition() -self.Missions={} -self:HandleEvent(EVENTS.Birth, -function(self,EventData) -if EventData.IniObjectCategory==1 then -local EventGroup=GROUP:Find(EventData.IniDCSGroup) -if EventGroup and self:HasGroup(EventGroup)then -local MenuReporting=MENU_GROUP:New(EventGroup,"Missions Reports",self.CommandCenterMenu) -local MenuMissionsSummary=MENU_GROUP_COMMAND:New(EventGroup,"Missions Status Report",MenuReporting,self.ReportMissionsStatus,self,EventGroup) -local MenuMissionsDetails=MENU_GROUP_COMMAND:New(EventGroup,"Missions Players Report",MenuReporting,self.ReportMissionsPlayers,self,EventGroup) -self:ReportSummary(EventGroup) -local PlayerUnit=EventData.IniUnit -for MissionID,Mission in pairs(self:GetMissions())do -local Mission=Mission -local PlayerGroup=EventData.IniGroup -Mission:JoinUnit(PlayerUnit,PlayerGroup) -end -self:SetMenu() -_DATABASE:PlayerSettingsMenu(PlayerUnit) -end -end -end -) -self:HandleEvent(EVENTS.PlayerEnterUnit, -function(self,EventData) -local PlayerUnit=EventData.IniUnit -for MissionID,Mission in pairs(self:GetMissions())do -local Mission=Mission -local PlayerGroup=EventData.IniGroup -Mission:JoinUnit(PlayerUnit,PlayerGroup) -end -self:SetMenu() -end -) -self:HandleEvent(EVENTS.MissionEnd, -function(self,EventData) -local PlayerUnit=EventData.IniUnit -for MissionID,Mission in pairs(self:GetMissions())do -local Mission=Mission -Mission:Stop() -end -end -) -self:HandleEvent(EVENTS.PlayerLeaveUnit, -function(self,EventData) -local PlayerUnit=EventData.IniUnit -for MissionID,Mission in pairs(self:GetMissions())do -local Mission=Mission -if Mission:IsENGAGED()then -Mission:AbortUnit(PlayerUnit) -end -end -end -) -self:HandleEvent(EVENTS.Crash, -function(self,EventData) -local PlayerUnit=EventData.IniUnit -for MissionID,Mission in pairs(self:GetMissions())do -local Mission=Mission -if Mission:IsENGAGED()then -Mission:CrashUnit(PlayerUnit) -end -end -end -) -self:SetMenu() -_SETTINGS:SetSystemMenu(CommandCenterPositionable) -return self -end -function COMMANDCENTER:GetName() -return self.CommandCenterName -end -function COMMANDCENTER:GetPositionable() -return self.CommandCenterPositionable -end -function COMMANDCENTER:GetMissions() -return self.Missions -end -function COMMANDCENTER:AddMission(Mission) -self.Missions[Mission]=Mission -return Mission -end -function COMMANDCENTER:RemoveMission(Mission) -self.Missions[Mission]=nil -return Mission -end -function COMMANDCENTER:SetReferenceZones(ReferenceZonePrefix) -local MatchPattern="(.*)#(.*)" -self:F({MatchPattern=MatchPattern}) -for ReferenceZoneName in pairs(_DATABASE.ZONENAMES)do -local ZoneName,ReferenceName=string.match(ReferenceZoneName,MatchPattern) -self:F({ZoneName=ZoneName,ReferenceName=ReferenceName}) -if ZoneName and ReferenceName and ZoneName==ReferenceZonePrefix then -self.ReferencePoints[ReferenceZoneName]=ZONE:New(ReferenceZoneName) -self.ReferenceNames[ReferenceZoneName]=ReferenceName -end -end -return self -end -function COMMANDCENTER:SetModeWWII() -self.CommunicationMode="WWII" -return self -end -function COMMANDCENTER:IsModeWWII() -return self.CommunicationMode=="WWII" -end -function COMMANDCENTER:SetMenu() -self:F() -self.CommandCenterMenu=self.CommandCenterMenu or MENU_COALITION:New(self.CommandCenterCoalition,"Command Center ("..self:GetName()..")") -local MenuTime=timer.getTime() -for MissionID,Mission in pairs(self:GetMissions()or{})do -local Mission=Mission -Mission:SetMenu(MenuTime) -end -for MissionID,Mission in pairs(self:GetMissions()or{})do -local Mission=Mission -Mission:RemoveMenu(MenuTime) -end -end -function COMMANDCENTER:GetMenu() -return self.CommandCenterMenu -end -function COMMANDCENTER:HasGroup(MissionGroup) -local Has=false -for MissionID,Mission in pairs(self.Missions)do -local Mission=Mission -if Mission:HasGroup(MissionGroup)then -Has=true -break -end -end -return Has -end -function COMMANDCENTER:MessageToAll(Message) -self:GetPositionable():MessageToAll(Message,20,self:GetName()) -end -function COMMANDCENTER:MessageToGroup(Message,TaskGroup) -self:GetPositionable():MessageToGroup(Message,15,TaskGroup,self:GetName()) -end -function COMMANDCENTER:MessageTypeToGroup(Message,TaskGroup,MessageType) -self:GetPositionable():MessageTypeToGroup(Message,MessageType,TaskGroup,self:GetName()) -end -function COMMANDCENTER:MessageToCoalition(Message) -local CCCoalition=self:GetPositionable():GetCoalition() -self:GetPositionable():MessageToCoalition(Message,15,CCCoalition) -end -function COMMANDCENTER:MessageTypeToCoalition(Message,MessageType) -local CCCoalition=self:GetPositionable():GetCoalition() -self:GetPositionable():MessageTypeToCoalition(Message,MessageType,CCCoalition) -end -function COMMANDCENTER:ReportMissionsStatus(ReportGroup) -self:E(ReportGroup) -local Report=REPORT:New() -Report:Add("Status report of all missions.") -for MissionID,Mission in pairs(self.Missions)do -local Mission=Mission -Report:Add(" - "..Mission:ReportStatus()) -end -self:MessageToGroup(Report:Text(),ReportGroup) -end -function COMMANDCENTER:ReportMissionsPlayers(ReportGroup) -self:E(ReportGroup) -local Report=REPORT:New() -Report:Add("Players active in all missions.") -for MissionID,Mission in pairs(self.Missions)do -local Mission=Mission -Report:Add(" - "..Mission:ReportPlayers()) -end -self:MessageToGroup(Report:Text(),ReportGroup) -end -function COMMANDCENTER:ReportDetails(ReportGroup,Task) -self:E(ReportGroup) -local Report=REPORT:New() -for MissionID,Mission in pairs(self.Missions)do -local Mission=Mission -Report:Add(" - "..Mission:ReportDetails()) -end -self:MessageToGroup(Report:Text(),ReportGroup) -end -MISSION={ -ClassName="MISSION", -Name="", -MissionStatus="PENDING", -AssignedGroups={}, -} -function MISSION:New(CommandCenter,MissionName,MissionPriority,MissionBriefing,MissionCoalition) -local self=BASE:Inherit(self,FSM:New()) -self:T({MissionName,MissionPriority,MissionBriefing,MissionCoalition}) -self.CommandCenter=CommandCenter -CommandCenter:AddMission(self) -self.Name=MissionName -self.MissionPriority=MissionPriority -self.MissionBriefing=MissionBriefing -self.MissionCoalition=MissionCoalition -self.Tasks={} -self.PlayerNames={} -self:SetStartState("IDLE") -self:AddTransition("IDLE","Start","ENGAGED") -self:AddTransition("ENGAGED","Stop","IDLE") -self:AddTransition("ENGAGED","Complete","COMPLETED") -self:AddTransition("*","Fail","FAILED") -self:AddTransition("*","MissionGoals","*") -CommandCenter:SetMenu() -return self -end -function MISSION:onenterCOMPLETED(From,Event,To) -self:GetCommandCenter():MessageTypeToCoalition(self:GetName().." has been completed! Good job guys!",MESSAGE.Type.Information) -end -function MISSION:GetName() -return string.format('Mission "%s (%s)"',self.Name,self.MissionPriority) -end -function MISSION:JoinUnit(PlayerUnit,PlayerGroup) -self:F({PlayerUnit=PlayerUnit,PlayerGroup=PlayerGroup}) -local PlayerUnitAdded=false -for TaskID,Task in pairs(self:GetTasks())do -local Task=Task -if Task:JoinUnit(PlayerUnit,PlayerGroup)then -PlayerUnitAdded=true -end -end -return PlayerUnitAdded -end -function MISSION:AbortUnit(PlayerUnit) -self:F({PlayerUnit=PlayerUnit}) -for TaskID,Task in pairs(self:GetTasks())do -local Task=Task -local PlayerGroup=PlayerUnit:GetGroup() -Task:AbortGroup(PlayerGroup) -end -return self -end -function MISSION:CrashUnit(PlayerUnit) -self:F({PlayerUnit=PlayerUnit}) -for TaskID,Task in pairs(self:GetTasks())do -local Task=Task -local PlayerGroup=PlayerUnit:GetGroup() -Task:CrashGroup(PlayerGroup) -end -return self -end -function MISSION:AddScoring(Scoring) -self.Scoring=Scoring -return self -end -function MISSION:GetScoring() -return self.Scoring -end -function MISSION:GetGroups() -local SetGroup=SET_GROUP:New() -for TaskID,Task in pairs(self:GetTasks())do -local Task=Task -local GroupSet=Task:GetGroups() -GroupSet:ForEachGroup( -function(TaskGroup) -SetGroup:Add(TaskGroup,TaskGroup) -end -) -end -return SetGroup -end -function MISSION:SetMenu(MenuTime) -self:F({self:GetName(),MenuTime}) -for _,TaskData in pairs(self:GetTasks())do -local Task=TaskData -Task:SetMenu(MenuTime) -end -end -function MISSION:RemoveMenu(MenuTime) -self:F({self:GetName(),MenuTime}) -for _,Task in pairs(self:GetTasks())do -local Task=Task -Task:RemoveMenu(MenuTime) -end -end -do -function MISSION:IsGroupAssigned(MissionGroup) -local MissionGroupName=MissionGroup:GetName() -if self.AssignedGroups[MissionGroupName]==MissionGroup then -self:T({"Mission is assigned to:",MissionGroup:GetName()}) -return true -end -self:T({"Mission is not assigned to:",MissionGroup:GetName()}) -return false -end -function MISSION:SetGroupAssigned(MissionGroup) -local MissionName=self:GetName() -local MissionGroupName=MissionGroup:GetName() -self.AssignedGroups[MissionGroupName]=MissionGroup -self:E(string.format("Mission %s is assigned to %s",MissionName,MissionGroupName)) -return self -end -function MISSION:ClearGroupAssignment(MissionGroup) -local MissionName=self:GetName() -local MissionGroupName=MissionGroup:GetName() -self.AssignedGroups[MissionGroupName]=nil -return self -end -end -function MISSION:GetCommandCenter() -return self.CommandCenter -end -function MISSION:RemoveTaskMenu(Task) -Task:RemoveMenu() -end -function MISSION:GetRootMenu(TaskGroup) -local CommandCenter=self:GetCommandCenter() -local CommandCenterMenu=CommandCenter:GetMenu() -local MissionName=self:GetName() -self.MissionMenu=self.MissionMenu or MENU_COALITION:New(self.MissionCoalition,self:GetName(),CommandCenterMenu) -return self.MissionMenu -end -function MISSION:GetMenu(TaskGroup) -local CommandCenter=self:GetCommandCenter() -local CommandCenterMenu=CommandCenter:GetMenu() -local MissionName=self:GetName() -self.MissionGroupMenu=self.MissionGroupMenu or{} -self.MissionGroupMenu[TaskGroup]=self.MissionGroupMenu[TaskGroup]or{} -local GroupMenu=self.MissionGroupMenu[TaskGroup] -self.MissionMenu=self.MissionMenu or MENU_COALITION:New(self.MissionCoalition,self:GetName(),CommandCenterMenu) -GroupMenu.BriefingMenu=GroupMenu.BriefingMenu or MENU_GROUP_COMMAND:New(TaskGroup,"Mission Briefing",self.MissionMenu,self.MenuReportBriefing,self,TaskGroup) -GroupMenu.TaskReportsMenu=GroupMenu.TaskReportsMenu or MENU_GROUP:New(TaskGroup,"Task Reports",self.MissionMenu) -GroupMenu.ReportTasksMenu=GroupMenu.ReportTasksMenu or MENU_GROUP_COMMAND:New(TaskGroup,"Report Tasks",GroupMenu.TaskReportsMenu,self.MenuReportTasksSummary,self,TaskGroup) -GroupMenu.ReportPlannedTasksMenu=GroupMenu.ReportPlannedTasksMenu or MENU_GROUP_COMMAND:New(TaskGroup,"Report Planned Tasks",GroupMenu.TaskReportsMenu,self.MenuReportTasksPerStatus,self,TaskGroup,"Planned") -GroupMenu.ReportAssignedTasksMenu=GroupMenu.ReportAssignedTasksMenu or MENU_GROUP_COMMAND:New(TaskGroup,"Report Assigned Tasks",GroupMenu.TaskReportsMenu,self.MenuReportTasksPerStatus,self,TaskGroup,"Assigned") -GroupMenu.ReportSuccessTasksMenu=GroupMenu.ReportSuccessTasksMenu or MENU_GROUP_COMMAND:New(TaskGroup,"Report Successful Tasks",GroupMenu.TaskReportsMenu,self.MenuReportTasksPerStatus,self,TaskGroup,"Success") -GroupMenu.ReportFailedTasksMenu=GroupMenu.ReportFailedTasksMenu or MENU_GROUP_COMMAND:New(TaskGroup,"Report Failed Tasks",GroupMenu.TaskReportsMenu,self.MenuReportTasksPerStatus,self,TaskGroup,"Failed") -GroupMenu.ReportHeldTasksMenu=GroupMenu.ReportHeldTasksMenu or MENU_GROUP_COMMAND:New(TaskGroup,"Report Held Tasks",GroupMenu.TaskReportsMenu,self.MenuReportTasksPerStatus,self,TaskGroup,"Hold") -GroupMenu.PlayerReportsMenu=GroupMenu.PlayerReportsMenu or MENU_GROUP:New(TaskGroup,"Statistics Reports",self.MissionMenu) -GroupMenu.ReportMissionHistory=GroupMenu.ReportPlayersHistory or MENU_GROUP_COMMAND:New(TaskGroup,"Report Mission Progress",GroupMenu.PlayerReportsMenu,self.MenuReportPlayersProgress,self,TaskGroup) -GroupMenu.ReportPlayersPerTaskMenu=GroupMenu.ReportPlayersPerTaskMenu or MENU_GROUP_COMMAND:New(TaskGroup,"Report Players per Task",GroupMenu.PlayerReportsMenu,self.MenuReportPlayersPerTask,self,TaskGroup) -return self.MissionMenu -end -function MISSION:GetTask(TaskName) -self:F({TaskName}) -return self.Tasks[TaskName] -end -function MISSION:AddTask(Task) -local TaskName=Task:GetTaskName() -self:F(TaskName) -self.Tasks[TaskName]=self.Tasks[TaskName]or{n=0} -self.Tasks[TaskName]=Task -self:GetCommandCenter():SetMenu() -return Task -end -function MISSION:RemoveTask(Task) -local TaskName=Task:GetTaskName() -self:F(TaskName) -self.Tasks[TaskName]=self.Tasks[TaskName]or{n=0} -self.Tasks[TaskName]=nil -Task=nil -collectgarbage() -self:GetCommandCenter():SetMenu() -return nil -end -function MISSION:GetNextTaskID(Task) -local TaskName=Task:GetTaskName() -self:F(TaskName) -self.Tasks[TaskName]=self.Tasks[TaskName]or{n=0} -self.Tasks[TaskName].n=self.Tasks[TaskName].n+1 -return self.Tasks[TaskName].n -end -function MISSION:IsCOMPLETED() -return self:Is("COMPLETED") -end -function MISSION:IsIDLE() -return self:Is("IDLE") -end -function MISSION:IsENGAGED() -return self:Is("ENGAGED") -end -function MISSION:IsFAILED() -return self:Is("FAILED") -end -function MISSION:IsHOLD() -return self:Is("HOLD") -end -function MISSION:HasGroup(TaskGroup) -local Has=false -for TaskID,Task in pairs(self:GetTasks())do -local Task=Task -if Task:HasGroup(TaskGroup)then -Has=true -break -end -end -return Has -end -function MISSION:GetTasksRemaining() -local TasksRemaining=0 -for TaskID,Task in pairs(self:GetTasks())do -local Task=Task -if Task:IsStateSuccess()or Task:IsStateFailed()then -else -TasksRemaining=TasksRemaining+1 -end -end -return TasksRemaining -end -function MISSION:GetTaskTypes() -local TaskTypeList={} -local TasksRemaining=0 -for TaskID,Task in pairs(self:GetTasks())do -local Task=Task -local TaskType=Task:GetType() -TaskTypeList[TaskType]=TaskType -end -return TaskTypeList -end -function MISSION:AddPlayerName(PlayerName) -self.PlayerNames=self.PlayerNames or{} -self.PlayerNames[PlayerName]=PlayerName -return self -end -function MISSION:GetPlayerNames() -return self.PlayerNames -end -function MISSION:ReportBriefing() -local Report=REPORT:New() -local Name=self:GetName() -local Status="<"..self:GetState()..">" -Report:Add(string.format('%s - %s - Mission Briefing Report',Name,Status)) -Report:Add(self.MissionBriefing) -return Report:Text() -end -function MISSION:ReportStatus() -local Report=REPORT:New() -local Name=self:GetName() -local Status="<"..self:GetState()..">" -Report:Add(string.format('%s - Status "%s"',Name,Status)) -local TaskTypes=self:GetTaskTypes() -Report:Add(string.format(" - Task Types: %s",table.concat(TaskTypes,", "))) -local TaskStatusList={"Planned","Assigned","Success","Hold","Cancelled","Aborted","Failed"} -for TaskStatusID,TaskStatus in pairs(TaskStatusList)do -local TaskCount=0 -local TaskPlayerCount=0 -for TaskID,Task in pairs(self:GetTasks())do -local Task=Task -if Task:Is(TaskStatus)then -TaskCount=TaskCount+1 -TaskPlayerCount=TaskPlayerCount+Task:GetPlayerCount() -end -end -if TaskCount>0 then -Report:Add(string.format(" - %02d %s Tasks (%dp)",TaskCount,TaskStatus,TaskPlayerCount)) -end -end -return Report:Text() -end -function MISSION:ReportPlayersPerTask(ReportGroup) -local Report=REPORT:New() -local Name=self:GetName() -local Status="<"..self:GetState()..">" -Report:Add(string.format('%s - %s - Players per Task Report',Name,Status)) -local PlayerList={} -for TaskID,Task in pairs(self:GetTasks())do -local Task=Task -local PlayerNames=Task:GetPlayerNames() -for PlayerName,PlayerGroup in pairs(PlayerNames)do -PlayerList[PlayerName]=Task:GetName() -end -end -for PlayerName,TaskName in pairs(PlayerList)do -Report:Add(string.format(' - Player (%s): Task "%s"',PlayerName,TaskName)) -end -return Report:Text() -end -function MISSION:ReportPlayersProgress(ReportGroup) -local Report=REPORT:New() -local Name=self:GetName() -local Status="<"..self:GetState()..">" -Report:Add(string.format('%s - %s - Players per Task Progress Report',Name,Status)) -local PlayerList={} -for TaskID,Task in pairs(self:GetTasks())do -local Task=Task -local TaskGoalTotal=Task:GetGoalTotal()or 0 -local TaskName=Task:GetName() -PlayerList[TaskName]=PlayerList[TaskName]or{} -if TaskGoalTotal~=0 then -local PlayerNames=self:GetPlayerNames() -for PlayerName,PlayerData in pairs(PlayerNames)do -PlayerList[TaskName][PlayerName]=string.format('Player (%s): Task "%s": %d%%',PlayerName,TaskName,Task:GetPlayerProgress(PlayerName)*100/TaskGoalTotal) -end -else -PlayerList[TaskName]["_"]=string.format('Player (---): Task "%s": %d%%',TaskName,0) -end -end -for TaskName,TaskData in pairs(PlayerList)do -for PlayerName,TaskText in pairs(TaskData)do -Report:Add(string.format(' - %s',TaskText)) -end -end -return Report:Text() -end -function MISSION:ReportSummary(ReportGroup) -local Report=REPORT:New() -local Name=self:GetName() -local Status="<"..self:GetState()..">" -Report:Add(string.format('%s - %s - Task Overview Report',Name,Status)) -for TaskID,Task in UTILS.spairs(self:GetTasks(),function(t,a,b)return t[a]:ReportOrder(ReportGroup)" -Report:Add(string.format('%s - %s - %s Tasks Report',Name,Status,TaskStatus)) -local Tasks=0 -for TaskID,Task in UTILS.spairs(self:GetTasks(),function(t,a,b)return t[a]:ReportOrder(ReportGroup)=8 then -break -end -end -return Report:Text() -end -function MISSION:ReportDetails(ReportGroup) -local Report=REPORT:New() -local Name=self:GetName() -local Status="<"..self:GetState()..">" -Report:Add(string.format('%s - %s - Task Detailed Report',Name,Status)) -local TasksRemaining=0 -for TaskID,Task in pairs(self:GetTasks())do -local Task=Task -Report:Add(Task:ReportDetails(ReportGroup)) -end -return Report:Text() -end -function MISSION:GetTasks() -return self.Tasks -end -function MISSION:MenuReportBriefing(ReportGroup) -local Report=self:ReportBriefing() -self:GetCommandCenter():MessageTypeToGroup(Report,ReportGroup,MESSAGE.Type.Briefing) -end -function MISSION:MenuReportTasksSummary(ReportGroup) -local Report=self:ReportSummary(ReportGroup) -self:GetCommandCenter():MessageTypeToGroup(Report,ReportGroup,MESSAGE.Type.Overview) -end -function MISSION:MenuReportTasksPerStatus(ReportGroup,TaskStatus) -local Report=self:ReportOverview(ReportGroup,TaskStatus) -self:GetCommandCenter():MessageTypeToGroup(Report,ReportGroup,MESSAGE.Type.Overview) -end -function MISSION:MenuReportPlayersPerTask(ReportGroup) -local Report=self:ReportPlayersPerTask() -self:GetCommandCenter():MessageTypeToGroup(Report,ReportGroup,MESSAGE.Type.Overview) -end -function MISSION:MenuReportPlayersProgress(ReportGroup) -local Report=self:ReportPlayersProgress() -self:GetCommandCenter():MessageTypeToGroup(Report,ReportGroup,MESSAGE.Type.Overview) -end -TASK={ -ClassName="TASK", -TaskScheduler=nil, -ProcessClasses={}, -Processes={}, -Players=nil, -Scores={}, -Menu={}, -SetGroup=nil, -FsmTemplate=nil, -Mission=nil, -CommandCenter=nil, -TimeOut=0, -AssignedGroups={}, -} -function TASK:New(Mission,SetGroupAssign,TaskName,TaskType,TaskBriefing) -local self=BASE:Inherit(self,FSM_TASK:New()) -self:SetStartState("Planned") -self:AddTransition("Planned","Assign","Assigned") -self:AddTransition("Assigned","AssignUnit","Assigned") -self:AddTransition("Assigned","Success","Success") -self:AddTransition("Assigned","Hold","Hold") -self:AddTransition("Assigned","Fail","Failed") -self:AddTransition("Assigned","Abort","Aborted") -self:AddTransition("Assigned","Cancel","Cancelled") -self:AddTransition("Assigned","Goal","*") -self:AddTransition("*","PlayerCrashed","*") -self:AddTransition("*","PlayerAborted","*") -self:AddTransition("*","PlayerDead","*") -self:AddTransition({"Failed","Aborted","Cancelled"},"Replan","Planned") -self:AddTransition("*","TimeOut","Cancelled") -self:E("New TASK "..TaskName) -self.Processes={} -self.Fsm={} -self.Mission=Mission -self.CommandCenter=Mission:GetCommandCenter() -self.SetGroup=SetGroupAssign -self:SetType(TaskType) -self:SetName(TaskName) -self:SetID(Mission:GetNextTaskID(self)) -self:SetBriefing(TaskBriefing) -self.FsmTemplate=self.FsmTemplate or FSM_PROCESS:New() -self.TaskInfo={} -self.TaskProgress={} -return self -end -function TASK:GetUnitProcess(TaskUnit) -if TaskUnit then -return self:GetStateMachine(TaskUnit) -else -return self.FsmTemplate -end -end -function TASK:SetUnitProcess(FsmTemplate) -self.FsmTemplate=FsmTemplate -end -function TASK:JoinUnit(PlayerUnit,PlayerGroup) -self:F({PlayerUnit=PlayerUnit,PlayerGroup=PlayerGroup}) -local PlayerUnitAdded=false -local PlayerGroups=self:GetGroups() -if PlayerGroups:IsIncludeObject(PlayerGroup)then -if self:IsStatePlanned()or self:IsStateReplanned()then -end -if self:IsStateAssigned()then -local IsGroupAssigned=self:IsGroupAssigned(PlayerGroup) -self:E({IsGroupAssigned=IsGroupAssigned}) -if IsGroupAssigned then -self:AssignToUnit(PlayerUnit) -self:MessageToGroups(PlayerUnit:GetPlayerName().." joined Task "..self:GetName()) -end -end -end -return PlayerUnitAdded -end -function TASK:AbortGroup(PlayerGroup) -self:F({PlayerGroup=PlayerGroup}) -local PlayerGroups=self:GetGroups() -if PlayerGroups:IsIncludeObject(PlayerGroup)then -if self:IsStateAssigned()then -local IsGroupAssigned=self:IsGroupAssigned(PlayerGroup) -self:E({IsGroupAssigned=IsGroupAssigned}) -if IsGroupAssigned then -local PlayerName=PlayerGroup:GetUnit(1):GetPlayerName() -self:UnAssignFromGroup(PlayerGroup) -PlayerGroups:Flush() -local IsRemaining=false -for GroupName,AssignedGroup in pairs(PlayerGroups:GetSet()or{})do -if self:IsGroupAssigned(AssignedGroup)==true then -IsRemaining=true -self:F({Task=self:GetName(),IsRemaining=IsRemaining}) -break -end -end -self:F({Task=self:GetName(),IsRemaining=IsRemaining}) -if IsRemaining==false then -self:Abort() -end -self:PlayerAborted(PlayerGroup:GetUnit(1)) -end -end -end -return self -end -function TASK:CrashGroup(PlayerGroup) -self:F({PlayerGroup=PlayerGroup}) -local PlayerGroups=self:GetGroups() -if PlayerGroups:IsIncludeObject(PlayerGroup)then -if self:IsStateAssigned()then -local IsGroupAssigned=self:IsGroupAssigned(PlayerGroup) -self:E({IsGroupAssigned=IsGroupAssigned}) -if IsGroupAssigned then -local PlayerName=PlayerGroup:GetUnit(1):GetPlayerName() -self:MessageToGroups(PlayerName.." crashed! ") -self:UnAssignFromGroup(PlayerGroup) -PlayerGroups:Flush() -local IsRemaining=false -for GroupName,AssignedGroup in pairs(PlayerGroups:GetSet()or{})do -if self:IsGroupAssigned(AssignedGroup)==true then -IsRemaining=true -self:F({Task=self:GetName(),IsRemaining=IsRemaining}) -break -end -end -self:F({Task=self:GetName(),IsRemaining=IsRemaining}) -if IsRemaining==false then -self:Abort() -end -self:PlayerCrashed(PlayerGroup:GetUnit(1)) -end -end -end -return self -end -function TASK:GetMission() -return self.Mission -end -function TASK:GetGroups() -return self.SetGroup -end -do -function TASK:IsGroupAssigned(TaskGroup) -local TaskGroupName=TaskGroup:GetName() -if self.AssignedGroups[TaskGroupName]then -self:T({"Task is assigned to:",TaskGroup:GetName()}) -return true -end -self:T({"Task is not assigned to:",TaskGroup:GetName()}) -return false -end -function TASK:SetGroupAssigned(TaskGroup) -local TaskName=self:GetName() -local TaskGroupName=TaskGroup:GetName() -self.AssignedGroups[TaskGroupName]=TaskGroup -self:E(string.format("Task %s is assigned to %s",TaskName,TaskGroupName)) -self:GetMission():SetGroupAssigned(TaskGroup) -local SetAssignedGroups=self:GetGroups() -return self -end -function TASK:ClearGroupAssignment(TaskGroup) -local TaskName=self:GetName() -local TaskGroupName=TaskGroup:GetName() -self.AssignedGroups[TaskGroupName]=nil -self:GetMission():ClearGroupAssignment(TaskGroup) -local SetAssignedGroups=self:GetGroups() -SetAssignedGroups:ForEachGroup( -function(AssignedGroup) -if self:IsGroupAssigned(AssignedGroup)then -else -end -end -) -return self -end -end -do -function TASK:AssignToGroup(TaskGroup) -self:F(TaskGroup:GetName()) -local TaskGroupName=TaskGroup:GetName() -local Mission=self:GetMission() -local CommandCenter=Mission:GetCommandCenter() -self:SetGroupAssigned(TaskGroup) -local TaskUnits=TaskGroup:GetUnits() -for UnitID,UnitData in pairs(TaskUnits)do -local TaskUnit=UnitData -local PlayerName=TaskUnit:GetPlayerName() -self:E(PlayerName) -if PlayerName~=nil and PlayerName~=""then -self:AssignToUnit(TaskUnit) -CommandCenter:MessageToGroup( -string.format('Task "%s": Briefing for player (%s):\n%s', -self:GetName(), -PlayerName, -self:GetBriefing() -),TaskGroup -) -end -end -CommandCenter:SetMenu() -return self -end -function TASK:UnAssignFromGroup(TaskGroup) -self:F2({TaskGroup=TaskGroup:GetName()}) -self:ClearGroupAssignment(TaskGroup) -local TaskUnits=TaskGroup:GetUnits() -for UnitID,UnitData in pairs(TaskUnits)do -local TaskUnit=UnitData -local PlayerName=TaskUnit:GetPlayerName() -if PlayerName~=nil and PlayerName~=""then -self:UnAssignFromUnit(TaskUnit) -end -end -local Mission=self:GetMission() -local CommandCenter=Mission:GetCommandCenter() -CommandCenter:SetMenu() -end -end -function TASK:HasGroup(FindGroup) -local SetAttackGroup=self:GetGroups() -return SetAttackGroup:FindGroup(FindGroup) -end -function TASK:AssignToUnit(TaskUnit) -self:F(TaskUnit:GetName()) -local FsmTemplate=self:GetUnitProcess() -local FsmUnit=self:SetStateMachine(TaskUnit,FsmTemplate:Copy(TaskUnit,self)) -FsmUnit:SetStartState("Planned") -FsmUnit:Accept() -return self -end -function TASK:UnAssignFromUnit(TaskUnit) -self:F(TaskUnit:GetName()) -self:RemoveStateMachine(TaskUnit) -return self -end -function TASK:SetTimeOut(Timer) -self:F(Timer) -self.TimeOut=Timer -self:__TimeOut(self.TimeOut) -return self -end -function TASK:MessageToGroups(Message) -self:F({Message=Message}) -local Mission=self:GetMission() -local CC=Mission:GetCommandCenter() -for TaskGroupName,TaskGroup in pairs(self.SetGroup:GetSet())do -local TaskGroup=TaskGroup -CC:MessageToGroup(Message,TaskGroup,TaskGroup:GetName()) -end -end -function TASK:SendBriefingToAssignedGroups() -self:F2() -for TaskGroupName,TaskGroup in pairs(self.SetGroup:GetSet())do -if self:IsGroupAssigned(TaskGroup)then -TaskGroup:Message(self.TaskBriefing,60) -end -end -end -function TASK:UnAssignFromGroups() -self:F2() -for TaskGroupName,TaskGroup in pairs(self.SetGroup:GetSet())do -if self:IsGroupAssigned(TaskGroup)then -self:UnAssignFromGroup(TaskGroup) -end -end -end -function TASK:HasAliveUnits() -self:F() -for TaskGroupID,TaskGroup in pairs(self.SetGroup:GetSet())do -if self:IsStateAssigned()then -if self:IsGroupAssigned(TaskGroup)then -for TaskUnitID,TaskUnit in pairs(TaskGroup:GetUnits())do -if TaskUnit:IsAlive()then -self:T({HasAliveUnits=true}) -return true -end -end -end -end -end -self:T({HasAliveUnits=false}) -return false -end -function TASK:SetMenu(MenuTime) -self:F({self:GetName(),MenuTime}) -for TaskGroupID,TaskGroupData in pairs(self.SetGroup:GetSet())do -local TaskGroup=TaskGroupData -if TaskGroup:IsAlive()and TaskGroup:GetPlayerNames()then -local Mission=self:GetMission() -local MissionMenu=Mission:GetMenu(TaskGroup) -if MissionMenu then -self:SetMenuForGroup(TaskGroup,MenuTime) -end -end -end -end -function TASK:SetMenuForGroup(TaskGroup,MenuTime) -if self:IsStatePlanned()or self:IsStateAssigned()then -self:SetPlannedMenuForGroup(TaskGroup,MenuTime) -if self:IsGroupAssigned(TaskGroup)then -self:SetAssignedMenuForGroup(TaskGroup,MenuTime) -end -end -end -function TASK:SetPlannedMenuForGroup(TaskGroup,MenuTime) -self:F(TaskGroup:GetName()) -local Mission=self:GetMission() -local MissionName=Mission:GetName() -local CommandCenter=Mission:GetCommandCenter() -local CommandCenterMenu=CommandCenter:GetMenu() -local TaskType=self:GetType() -local TaskPlayerCount=self:GetPlayerCount() -local TaskPlayerString=string.format(" (%dp)",TaskPlayerCount) -local TaskText=string.format("%s%s",self:GetName(),TaskPlayerString) -local TaskName=string.format("%s",self:GetName()) -local MissionMenu=Mission:GetMenu(TaskGroup) -self.MenuPlanned=self.MenuPlanned or{} -self.MenuPlanned[TaskGroup]=MENU_GROUP:New(TaskGroup,"Join Planned Task",MissionMenu,Mission.MenuReportTasksPerStatus,Mission,TaskGroup,"Planned"):SetTime(MenuTime):SetTag("Tasking") -local TaskTypeMenu=MENU_GROUP:New(TaskGroup,TaskType,self.MenuPlanned[TaskGroup]):SetTime(MenuTime):SetTag("Tasking"):SetRemoveParent(true) -local TaskTypeMenu=MENU_GROUP:New(TaskGroup,TaskText,TaskTypeMenu):SetTime(MenuTime):SetTag("Tasking"):SetRemoveParent(true) -local ReportTaskMenu=MENU_GROUP_COMMAND:New(TaskGroup,string.format("Report Task Status"),TaskTypeMenu,self.MenuTaskStatus,self,TaskGroup):SetTime(MenuTime):SetTag("Tasking"):SetRemoveParent(true) -if not Mission:IsGroupAssigned(TaskGroup)then -self:F({"Replacing Join Task menu"}) -local JoinTaskMenu=MENU_GROUP_COMMAND:New(TaskGroup,string.format("Join Task"),TaskTypeMenu,self.MenuAssignToGroup,self,TaskGroup):SetTime(MenuTime):SetTag("Tasking"):SetRemoveParent(true) -local MarkTaskMenu=MENU_GROUP_COMMAND:New(TaskGroup,string.format("Mark Task on Map"),TaskTypeMenu,self.MenuMarkToGroup,self,TaskGroup):SetTime(MenuTime):SetTag("Tasking"):SetRemoveParent(true) -end -return self -end -function TASK:SetAssignedMenuForGroup(TaskGroup,MenuTime) -self:F({TaskGroup:GetName(),MenuTime}) -local Mission=self:GetMission() -local MissionName=Mission:GetName() -local CommandCenter=Mission:GetCommandCenter() -local CommandCenterMenu=CommandCenter:GetMenu() -local TaskType=self:GetType() -local TaskPlayerCount=self:GetPlayerCount() -local TaskPlayerString=string.format(" (%dp)",TaskPlayerCount) -local TaskText=string.format("%s%s",self:GetName(),TaskPlayerString) -local TaskName=string.format("%s",self:GetName()) -local MissionMenu=Mission:GetMenu(TaskGroup) -self.MenuAssigned=self.MenuAssigned or{} -self.MenuAssigned[TaskGroup]=MENU_GROUP:New(TaskGroup,string.format("Assigned Task %s",TaskName),MissionMenu):SetTime(MenuTime):SetTag("Tasking") -local TaskTypeMenu=MENU_GROUP_COMMAND:New(TaskGroup,string.format("Report Task Status"),self.MenuAssigned[TaskGroup],self.MenuTaskStatus,self,TaskGroup):SetTime(MenuTime):SetTag("Tasking"):SetRemoveParent(true) -local TaskMenu=MENU_GROUP_COMMAND:New(TaskGroup,string.format("Abort Group from Task"),self.MenuAssigned[TaskGroup],self.MenuTaskAbort,self,TaskGroup):SetTime(MenuTime):SetTag("Tasking"):SetRemoveParent(true) -return self -end -function TASK:RemoveMenu(MenuTime) -self:F({self:GetName(),MenuTime}) -for TaskGroupID,TaskGroup in pairs(self.SetGroup:GetSet())do -local TaskGroup=TaskGroup -self:RefreshMenus(TaskGroup,MenuTime) -end -end -function TASK:RefreshMenus(TaskGroup,MenuTime) -self:F({TaskGroup:GetName(),MenuTime}) -local Mission=self:GetMission() -local MissionName=Mission:GetName() -local CommandCenter=Mission:GetCommandCenter() -local CommandCenterMenu=CommandCenter:GetMenu() -local MissionMenu=Mission:GetMenu(TaskGroup) -local TaskName=self:GetName() -self.MenuPlanned=self.MenuPlanned or{} -local PlannedMenu=self.MenuPlanned[TaskGroup] -self.MenuAssigned=self.MenuAssigned or{} -local AssignedMenu=self.MenuAssigned[TaskGroup] -if PlannedMenu then -PlannedMenu:Remove(MenuTime,"Tasking") -end -if AssignedMenu then -AssignedMenu:Remove(MenuTime,"Tasking") -end -end -function TASK:RemoveAssignedMenuForGroup(TaskGroup) -self:F() -local Mission=self:GetMission() -local MissionName=Mission:GetName() -local MissionMenu=Mission:GetMenu(TaskGroup) -if MissionMenu then -MissionMenu:RemoveSubMenus() -end -end -function TASK:MenuAssignToGroup(TaskGroup) -self:E("Join Task menu selected") -self:AssignToGroup(TaskGroup) -end -function TASK:MenuMarkToGroup(TaskGroup) -self:E("Mark Task menu selected") -self:UpdateTaskInfo() -local Report=REPORT:New():SetIndent(0) -local Name=self:GetName() -Report:Add(Name..": "..self:GetTaskBriefing()) -for TaskInfoID,TaskInfo in pairs(self.TaskInfo,function(t,a,b)return t[a].TaskInfoOrder") -if self.TaskInfo["Coordinates"]then -local TaskInfoIDText=string.format("%s: ","Coordinate") -local TaskCoord=self.TaskInfo["Coordinates"].TaskInfoText -Report:Add(TaskInfoIDText..TaskCoord:ToString(ReportGroup,nil,self)) -end -return Report:Text(', ') -end -function TASK:ReportOverview(ReportGroup) -self:UpdateTaskInfo() -local TaskName=self:GetName() -local Report=REPORT:New() -local Line=0 -local LineReport=REPORT:New() -for TaskInfoID,TaskInfo in UTILS.spairs(self.TaskInfo,function(t,a,b)return t[a].TaskInfoOrder" -Report:Add("Task: "..Name.." - "..Status.." - Detailed Report") -local PlayerNames=self:GetPlayerNames() -local PlayerReport=REPORT:New() -for PlayerName,PlayerGroup in pairs(PlayerNames)do -PlayerReport:Add("Group "..PlayerGroup:GetCallsign()..": "..PlayerName) -end -local Players=PlayerReport:Text() -if Players~=""then -Report:Add(" - Players assigned:") -Report:AddIndent(Players) -end -for TaskInfoID,TaskInfo in pairs(self.TaskInfo,function(t,a,b)return t[a].TaskInfoOrder0 then -local TargetSetUnit=SET_UNIT:New() -TargetSetUnit:SetDatabase(DetectedSet) -TargetSetUnit:FilterHasSEAD() -TargetSetUnit:FilterOnce() -return TargetSetUnit -end -return nil -end -function TASK_A2G_DISPATCHER:EvaluateCAS(DetectedItem) -self:F({DetectedItem.ItemID}) -local DetectedSet=DetectedItem.Set -local DetectedZone=DetectedItem.Zone -local GroundUnitCount=DetectedSet:HasGroundUnits() -local FriendliesNearBy=self.Detection:IsFriendliesNearBy(DetectedItem) -local RadarCount=DetectedSet:HasSEAD() -if RadarCount==0 and GroundUnitCount>0 and FriendliesNearBy==true then -local TargetSetUnit=SET_UNIT:New() -TargetSetUnit:SetDatabase(DetectedSet) -TargetSetUnit:FilterOnce() -return TargetSetUnit -end -return nil -end -function TASK_A2G_DISPATCHER:EvaluateBAI(DetectedItem,FriendlyCoalition) -self:F({DetectedItem.ItemID}) -local DetectedSet=DetectedItem.Set -local DetectedZone=DetectedItem.Zone -local GroundUnitCount=DetectedSet:HasGroundUnits() -local FriendliesNearBy=self.Detection:IsFriendliesNearBy(DetectedItem) -local RadarCount=DetectedSet:HasSEAD() -if RadarCount==0 and GroundUnitCount>0 and FriendliesNearBy==false then -local TargetSetUnit=SET_UNIT:New() -TargetSetUnit:SetDatabase(DetectedSet) -TargetSetUnit:FilterOnce() -return TargetSetUnit -end -return nil -end -function TASK_A2G_DISPATCHER:RemoveTask(TaskIndex) -self.Mission:RemoveTask(self.Tasks[TaskIndex]) -self.Tasks[TaskIndex]=nil -end -function TASK_A2G_DISPATCHER:EvaluateRemoveTask(Mission,Task,TaskIndex,DetectedItemChanged) -if Task then -if(Task:IsStatePlanned()and DetectedItemChanged==true)or Task:IsStateCancelled()then -self:RemoveTask(TaskIndex) -end -end -return Task -end -function TASK_A2G_DISPATCHER:ProcessDetected(Detection) -self:E() -local AreaMsg={} -local TaskMsg={} -local ChangeMsg={} -local Mission=self.Mission -if Mission:IsIDLE()or Mission:IsENGAGED()then -local TaskReport=REPORT:New() -for TaskIndex,TaskData in pairs(self.Tasks)do -local Task=TaskData -if Task:IsStatePlanned()then -local DetectedItem=Detection:GetDetectedItem(TaskIndex) -if not DetectedItem then -local TaskText=Task:GetName() -for TaskGroupID,TaskGroup in pairs(self.SetGroup:GetSet())do -Mission:GetCommandCenter():MessageToGroup(string.format("Obsolete A2G task %s for %s removed.",TaskText,Mission:GetName()),TaskGroup) -end -Task=self:RemoveTask(TaskIndex) -Mission:RemoveTask(Task) -self.Tasks[TaskIndex]=nil -end -end -end -for DetectedItemID,DetectedItem in pairs(Detection:GetDetectedItems())do -local DetectedItem=DetectedItem -local DetectedSet=DetectedItem.Set -local DetectedZone=DetectedItem.Zone -local DetectedItemID=DetectedItem.ID -local TaskIndex=DetectedItem.Index -local DetectedItemChanged=DetectedItem.Changed -self:E({DetectedItemChanged=DetectedItemChanged,DetectedItemID=DetectedItemID,TaskIndex=TaskIndex}) -local Task=self.Tasks[TaskIndex] -if Task then -if Task:IsStateAssigned()then -if DetectedItemChanged==true then -local TargetsReport=REPORT:New() -local TargetSetUnit=self:EvaluateSEAD(DetectedItem) -if TargetSetUnit then -if Task:IsInstanceOf(TASK_A2G_SEAD)then -Task:SetTargetSetUnit(TargetSetUnit) -Task:UpdateTaskInfo() -TargetsReport:Add(Detection:GetChangeText(DetectedItem)) -else -Task:Cancel() -end -else -local TargetSetUnit=self:EvaluateCAS(DetectedItem) -if TargetSetUnit then -if Task:IsInstanceOf(TASK_A2G_CAS)then -Task:SetTargetSetUnit(TargetSetUnit) -Task:SetDetection(Detection,TaskIndex) -Task:UpdateTaskInfo() -TargetsReport:Add(Detection:GetChangeText(DetectedItem)) -else -Task:Cancel() -Task=self:RemoveTask(TaskIndex) -end -else -local TargetSetUnit=self:EvaluateBAI(DetectedItem) -if TargetSetUnit then -if Task:IsInstanceOf(TASK_A2G_BAI)then -Task:SetTargetSetUnit(TargetSetUnit) -Task:SetDetection(Detection,TaskIndex) -Task:UpdateTaskInfo() -TargetsReport:Add(Detection:GetChangeText(DetectedItem)) -else -Task:Cancel() -Task=self:RemoveTask(TaskIndex) -end -end -end -end -for TaskGroupID,TaskGroup in pairs(self.SetGroup:GetSet())do -local TargetsText=TargetsReport:Text(", ") -if(Mission:IsGroupAssigned(TaskGroup))and TargetsText~=""then -Mission:GetCommandCenter():MessageToGroup(string.format("Task %s has change of targets:\n %s",Task:GetName(),TargetsText),TaskGroup) -end -end -end -end -end -if Task then -if Task:IsStatePlanned()then -if DetectedItemChanged==true then -if Task:IsInstanceOf(TASK_A2G_SEAD)then -local TargetSetUnit=self:EvaluateSEAD(DetectedItem) -if TargetSetUnit then -Task:SetTargetSetUnit(TargetSetUnit) -Task:UpdateTaskInfo() -else -Task:Cancel() -Task=self:RemoveTask(TaskIndex) -end -else -if Task:IsInstanceOf(TASK_A2G_CAS)then -local TargetSetUnit=self:EvaluateCAS(DetectedItem) -if TargetSetUnit then -Task:SetTargetSetUnit(TargetSetUnit) -Task:SetDetection(Detection,TaskIndex) -Task:UpdateTaskInfo() -else -Task:Cancel() -Task=self:RemoveTask(TaskIndex) -end -else -if Task:IsInstanceOf(TASK_A2G_BAI)then -local TargetSetUnit=self:EvaluateBAI(DetectedItem) -if TargetSetUnit then -Task:SetTargetSetUnit(TargetSetUnit) -Task:SetDetection(Detection,TaskIndex) -Task:UpdateTaskInfo() -else -Task:Cancel() -Task=self:RemoveTask(TaskIndex) -end -else -Task:Cancel() -Task=self:RemoveTask(TaskIndex) -end -end -end -end -end -end -if not Task then -local TargetSetUnit=self:EvaluateSEAD(DetectedItem) -if TargetSetUnit then -Task=TASK_A2G_SEAD:New(Mission,self.SetGroup,string.format("SEAD.%03d",DetectedItemID),TargetSetUnit) -Task:SetDetection(Detection,TaskIndex) -end -if not Task then -local TargetSetUnit=self:EvaluateCAS(DetectedItem) -if TargetSetUnit then -Task=TASK_A2G_CAS:New(Mission,self.SetGroup,string.format("CAS.%03d",DetectedItemID),TargetSetUnit) -Task:SetDetection(Detection,TaskIndex) -end -if not Task then -local TargetSetUnit=self:EvaluateBAI(DetectedItem,self.Mission:GetCommandCenter():GetPositionable():GetCoalition()) -if TargetSetUnit then -Task=TASK_A2G_BAI:New(Mission,self.SetGroup,string.format("BAI.%03d",DetectedItemID),TargetSetUnit) -Task:SetDetection(Detection,TaskIndex) -end -end -end -if Task then -self.Tasks[TaskIndex]=Task -Task:SetTargetZone(DetectedZone) -Task:SetDispatcher(self) -Task:UpdateTaskInfo() -Mission:AddTask(Task) -TaskReport:Add(Task:GetName()) -else -self:E("This should not happen") -end -end -Detection:AcceptChanges(DetectedItem) -end -Mission:GetCommandCenter():SetMenu() -local TaskText=TaskReport:Text(", ") -for TaskGroupID,TaskGroup in pairs(self.SetGroup:GetSet())do -if(not Mission:IsGroupAssigned(TaskGroup))and TaskText~=""then -Mission:GetCommandCenter():MessageToGroup(string.format("%s has tasks %s. Subscribe to a task using the radio menu.",Mission:GetName(),TaskText),TaskGroup) -end -end -end -return true -end -end -do -TASK_A2G={ -ClassName="TASK_A2G", -} -function TASK_A2G:New(Mission,SetGroup,TaskName,TargetSetUnit,TaskType,TaskBriefing) -local self=BASE:Inherit(self,TASK:New(Mission,SetGroup,TaskName,TaskType,TaskBriefing)) -self:F() -self.TargetSetUnit=TargetSetUnit -self.TaskType=TaskType -local Fsm=self:GetUnitProcess() -Fsm:AddProcess("Planned","Accept",ACT_ASSIGN_ACCEPT:New(self.TaskBriefing),{Assigned="RouteToRendezVous",Rejected="Reject"}) -Fsm:AddTransition("Assigned","RouteToRendezVous","RoutingToRendezVous") -Fsm:AddProcess("RoutingToRendezVous","RouteToRendezVousPoint",ACT_ROUTE_POINT:New(),{Arrived="ArriveAtRendezVous"}) -Fsm:AddProcess("RoutingToRendezVous","RouteToRendezVousZone",ACT_ROUTE_ZONE:New(),{Arrived="ArriveAtRendezVous"}) -Fsm:AddTransition({"Arrived","RoutingToRendezVous"},"ArriveAtRendezVous","ArrivedAtRendezVous") -Fsm:AddTransition({"ArrivedAtRendezVous","HoldingAtRendezVous"},"Engage","Engaging") -Fsm:AddTransition({"ArrivedAtRendezVous","HoldingAtRendezVous"},"HoldAtRendezVous","HoldingAtRendezVous") -Fsm:AddProcess("Engaging","Account",ACT_ACCOUNT_DEADS:New(),{}) -Fsm:AddTransition("Engaging","RouteToTarget","Engaging") -Fsm:AddProcess("Engaging","RouteToTargetZone",ACT_ROUTE_ZONE:New(),{}) -Fsm:AddProcess("Engaging","RouteToTargetPoint",ACT_ROUTE_POINT:New(),{}) -Fsm:AddTransition("Engaging","RouteToTargets","Engaging") -Fsm:AddTransition("Rejected","Reject","Aborted") -Fsm:AddTransition("Failed","Fail","Failed") -function Fsm:onafterRouteToRendezVous(TaskUnit,Task) -self:E({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) -if Task:GetRendezVousZone(TaskUnit)then -self:__RouteToRendezVousZone(0.1) -else -if Task:GetRendezVousCoordinate(TaskUnit)then -self:__RouteToRendezVousPoint(0.1) -else -self:__ArriveAtRendezVous(0.1) -end -end -end -function Fsm:OnAfterArriveAtRendezVous(TaskUnit,Task) -self:E({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) -self:__Engage(0.1) -end -function Fsm:onafterEngage(TaskUnit,Task) -self:E({self}) -self:__Account(0.1) -self:__RouteToTarget(0.1) -self:__RouteToTargets(-10) -end -function Fsm:onafterRouteToTarget(TaskUnit,Task) -self:E({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) -if Task:GetTargetZone(TaskUnit)then -self:__RouteToTargetZone(0.1) -else -local TargetUnit=Task.TargetSetUnit:GetFirst() -if TargetUnit then -local Coordinate=TargetUnit:GetPointVec3() -self:T({TargetCoordinate=Coordinate,Coordinate:GetX(),Coordinate:GetY(),Coordinate:GetZ()}) -Task:SetTargetCoordinate(Coordinate,TaskUnit) -end -self:__RouteToTargetPoint(0.1) -end -end -function Fsm:onafterRouteToTargets(TaskUnit,Task) -self:E({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) -local TargetUnit=Task.TargetSetUnit:GetFirst() -if TargetUnit then -Task:SetTargetCoordinate(TargetUnit:GetCoordinate(),TaskUnit) -end -self:__RouteToTargets(-10) -end -return self -end -function TASK_A2G:SetTargetSetUnit(TargetSetUnit) -self.TargetSetUnit=TargetSetUnit -end -function TASK_A2G:GetPlannedMenuText() -return self:GetStateString().." - "..self:GetTaskName().." ( "..self.TargetSetUnit:GetUnitTypesText().." )" -end -function TASK_A2G:SetRendezVousCoordinate(RendezVousCoordinate,RendezVousRange,TaskUnit) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -local ActRouteRendezVous=ProcessUnit:GetProcess("RoutingToRendezVous","RouteToRendezVousPoint") -ActRouteRendezVous:SetCoordinate(RendezVousCoordinate) -ActRouteRendezVous:SetRange(RendezVousRange) -end -function TASK_A2G:GetRendezVousCoordinate(TaskUnit) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -local ActRouteRendezVous=ProcessUnit:GetProcess("RoutingToRendezVous","RouteToRendezVousPoint") -return ActRouteRendezVous:GetCoordinate(),ActRouteRendezVous:GetRange() -end -function TASK_A2G:SetRendezVousZone(RendezVousZone,TaskUnit) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -local ActRouteRendezVous=ProcessUnit:GetProcess("RoutingToRendezVous","RouteToRendezVousZone") -ActRouteRendezVous:SetZone(RendezVousZone) -end -function TASK_A2G:GetRendezVousZone(TaskUnit) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -local ActRouteRendezVous=ProcessUnit:GetProcess("RoutingToRendezVous","RouteToRendezVousZone") -return ActRouteRendezVous:GetZone() -end -function TASK_A2G:SetTargetCoordinate(TargetCoordinate,TaskUnit) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -local ActRouteTarget=ProcessUnit:GetProcess("Engaging","RouteToTargetPoint") -ActRouteTarget:SetCoordinate(TargetCoordinate) -end -function TASK_A2G:GetTargetCoordinate(TaskUnit) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -local ActRouteTarget=ProcessUnit:GetProcess("Engaging","RouteToTargetPoint") -return ActRouteTarget:GetCoordinate() -end -function TASK_A2G:SetTargetZone(TargetZone,TaskUnit) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -local ActRouteTarget=ProcessUnit:GetProcess("Engaging","RouteToTargetZone") -ActRouteTarget:SetZone(TargetZone) -end -function TASK_A2G:GetTargetZone(TaskUnit) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -local ActRouteTarget=ProcessUnit:GetProcess("Engaging","RouteToTargetZone") -return ActRouteTarget:GetZone() -end -function TASK_A2G:SetGoalTotal() -self.GoalTotal=self.TargetSetUnit:Count() -end -function TASK_A2G:GetGoalTotal() -return self.GoalTotal -end -end -do -TASK_A2G_SEAD={ -ClassName="TASK_A2G_SEAD", -} -function TASK_A2G_SEAD:New(Mission,SetGroup,TaskName,TargetSetUnit,TaskBriefing) -local self=BASE:Inherit(self,TASK_A2G:New(Mission,SetGroup,TaskName,TargetSetUnit,"SEAD",TaskBriefing)) -self:F() -Mission:AddTask(self) -self:SetBriefing( -TaskBriefing or -"Execute a Suppression of Enemy Air Defenses." -) -return self -end -function TASK_A2G_SEAD:UpdateTaskInfo() -local TargetCoordinate=self.Detection and self.Detection:GetDetectedItemCoordinate(self.DetectedItemIndex)or self.TargetSetUnit:GetFirst():GetCoordinate() -self:SetInfo("Coordinates",TargetCoordinate,0) -local ThreatLevel,ThreatText -if self.Detection then -ThreatLevel,ThreatText=self.Detection:GetDetectedItemThreatLevel(self.DetectedItemIndex) -else -ThreatLevel,ThreatText=self.TargetSetUnit:CalculateThreatLevelA2G() -end -self:SetInfo("Threat",ThreatText.." ["..string.rep("■",ThreatLevel).."]",11) -if self.Detection then -local DetectedItemsCount=self.TargetSetUnit:Count() -local ReportTypes=REPORT:New() -local TargetTypes={} -for TargetUnitName,TargetUnit in pairs(self.TargetSetUnit:GetSet())do -local TargetType=self.Detection:GetDetectedUnitTypeName(TargetUnit) -if not TargetTypes[TargetType]then -TargetTypes[TargetType]=TargetType -ReportTypes:Add(TargetType) -end -end -self:SetInfo("Targets",string.format("%d of %s",DetectedItemsCount,ReportTypes:Text(", ")),10) -else -local DetectedItemsCount=self.TargetSetUnit:Count() -local DetectedItemsTypes=self.TargetSetUnit:GetTypeNames() -self:SetInfo("Targets",string.format("%d of %s",DetectedItemsCount,DetectedItemsTypes),10) -end -end -function TASK_A2G_SEAD:ReportOrder(ReportGroup) -local Coordinate=self:GetInfo("Coordinates") -local Distance=ReportGroup:GetCoordinate():Get2DDistance(Coordinate) -return Distance -end -function TASK_A2G_SEAD:onafterGoal(TaskUnit,From,Event,To) -local TargetSetUnit=self.TargetSetUnit -if TargetSetUnit:Count()==0 then -self:Success() -end -self:__Goal(-10) -end -function TASK_A2G_SEAD:SetScoreOnProgress(PlayerName,Score,TaskUnit) -self:F({PlayerName,Score,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScoreProcess("Engaging","Account","AccountForPlayer","Player "..PlayerName.." has SEADed a target.",Score) -return self -end -function TASK_A2G_SEAD:SetScoreOnSuccess(PlayerName,Score,TaskUnit) -self:F({PlayerName,Score,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScore("Success","All radar emitting targets have been successfully SEADed!",Score) -return self -end -function TASK_A2G_SEAD:SetScoreOnFail(PlayerName,Penalty,TaskUnit) -self:F({PlayerName,Penalty,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScore("Failed","The SEADing has failed!",Penalty) -return self -end -end -do -TASK_A2G_BAI={ -ClassName="TASK_A2G_BAI", -} -function TASK_A2G_BAI:New(Mission,SetGroup,TaskName,TargetSetUnit,TaskBriefing) -local self=BASE:Inherit(self,TASK_A2G:New(Mission,SetGroup,TaskName,TargetSetUnit,"BAI",TaskBriefing)) -self:F() -Mission:AddTask(self) -self:SetBriefing( -TaskBriefing or -"Execute a Battlefield Air Interdiction of a group of enemy targets." -) -return self -end -function TASK_A2G_BAI:UpdateTaskInfo() -self:E({self.Detection,self.DetectedItemIndex}) -local TargetCoordinate=self.Detection and self.Detection:GetDetectedItemCoordinate(self.DetectedItemIndex)or self.TargetSetUnit:GetFirst():GetCoordinate() -self:SetInfo("Coordinates",TargetCoordinate,0) -local ThreatLevel,ThreatText -if self.Detection then -ThreatLevel,ThreatText=self.Detection:GetDetectedItemThreatLevel(self.DetectedItemIndex) -else -ThreatLevel,ThreatText=self.TargetSetUnit:CalculateThreatLevelA2G() -end -self:SetInfo("Threat",ThreatText.." ["..string.rep("■",ThreatLevel).."]",11) -if self.Detection then -local DetectedItemsCount=self.TargetSetUnit:Count() -local ReportTypes=REPORT:New() -local TargetTypes={} -for TargetUnitName,TargetUnit in pairs(self.TargetSetUnit:GetSet())do -local TargetType=self.Detection:GetDetectedUnitTypeName(TargetUnit) -if not TargetTypes[TargetType]then -TargetTypes[TargetType]=TargetType -ReportTypes:Add(TargetType) -end -end -self:SetInfo("Targets",string.format("%d of %s",DetectedItemsCount,ReportTypes:Text(", ")),10) -else -local DetectedItemsCount=self.TargetSetUnit:Count() -local DetectedItemsTypes=self.TargetSetUnit:GetTypeNames() -self:SetInfo("Targets",string.format("%d of %s",DetectedItemsCount,DetectedItemsTypes),10) -end -end -function TASK_A2G_BAI:ReportOrder(ReportGroup) -local Coordinate=self:GetInfo("Coordinates") -local Distance=ReportGroup:GetCoordinate():Get2DDistance(Coordinate) -return Distance -end -function TASK_A2G_BAI:onafterGoal(TaskUnit,From,Event,To) -local TargetSetUnit=self.TargetSetUnit -if TargetSetUnit:Count()==0 then -self:Success() -end -self:__Goal(-10) -end -function TASK_A2G_BAI:SetScoreOnProgress(PlayerName,Score,TaskUnit) -self:F({PlayerName,Score,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScoreProcess("Engaging","Account","AccountForPlayer","Player "..PlayerName.." has destroyed a target in Battlefield Air Interdiction (BAI).",Score) -return self -end -function TASK_A2G_BAI:SetScoreOnSuccess(PlayerName,Score,TaskUnit) -self:F({PlayerName,Score,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScore("Success","All targets have been successfully destroyed! The Battlefield Air Interdiction (BAI) is a success!",Score) -return self -end -function TASK_A2G_BAI:SetScoreOnFail(PlayerName,Penalty,TaskUnit) -self:F({PlayerName,Penalty,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScore("Failed","The Battlefield Air Interdiction (BAI) has failed!",Penalty) -return self -end -end -do -TASK_A2G_CAS={ -ClassName="TASK_A2G_CAS", -} -function TASK_A2G_CAS:New(Mission,SetGroup,TaskName,TargetSetUnit,TaskBriefing) -local self=BASE:Inherit(self,TASK_A2G:New(Mission,SetGroup,TaskName,TargetSetUnit,"CAS",TaskBriefing)) -self:F() -Mission:AddTask(self) -self:SetBriefing( -TaskBriefing or -"Execute a Close Air Support for a group of enemy targets. ".. -"Beware of friendlies at the vicinity! " -) -return self -end -function TASK_A2G_CAS:UpdateTaskInfo() -local TargetCoordinate=(self.Detection and self.Detection:GetDetectedItemCoordinate(self.DetectedItemIndex))or self.TargetSetUnit:GetFirst():GetCoordinate() -self:SetInfo("Coordinates",TargetCoordinate,0) -local ThreatLevel,ThreatText -if self.Detection then -ThreatLevel,ThreatText=self.Detection:GetDetectedItemThreatLevel(self.DetectedItemIndex) -else -ThreatLevel,ThreatText=self.TargetSetUnit:CalculateThreatLevelA2G() -end -self:SetInfo("Threat",ThreatText.." ["..string.rep("■",ThreatLevel).."]",11) -if self.Detection then -local DetectedItemsCount=self.TargetSetUnit:Count() -local ReportTypes=REPORT:New() -local TargetTypes={} -for TargetUnitName,TargetUnit in pairs(self.TargetSetUnit:GetSet())do -local TargetType=self.Detection:GetDetectedUnitTypeName(TargetUnit) -if not TargetTypes[TargetType]then -TargetTypes[TargetType]=TargetType -ReportTypes:Add(TargetType) -end -end -self:SetInfo("Targets",string.format("%d of %s",DetectedItemsCount,ReportTypes:Text(", ")),10) -else -local DetectedItemsCount=self.TargetSetUnit:Count() -local DetectedItemsTypes=self.TargetSetUnit:GetTypeNames() -self:SetInfo("Targets",string.format("%d of %s",DetectedItemsCount,DetectedItemsTypes),10) -end -end -function TASK_A2G_CAS:ReportOrder(ReportGroup) -local Coordinate=self:GetInfo("Coordinates") -local Distance=ReportGroup:GetCoordinate():Get2DDistance(Coordinate) -return Distance -end -function TASK_A2G_CAS:onafterGoal(TaskUnit,From,Event,To) -local TargetSetUnit=self.TargetSetUnit -if TargetSetUnit:Count()==0 then -self:Success() -end -self:__Goal(-10) -end -function TASK_A2G_CAS:SetScoreOnProgress(PlayerName,Score,TaskUnit) -self:F({PlayerName,Score,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScoreProcess("Engaging","Account","AccountForPlayer","Player "..PlayerName.." has destroyed a target in Close Air Support (CAS).",Score) -return self -end -function TASK_A2G_CAS:SetScoreOnSuccess(PlayerName,Score,TaskUnit) -self:F({PlayerName,Score,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScore("Success","All targets have been successfully destroyed! The Close Air Support (CAS) was a success!",Score) -return self -end -function TASK_A2G_CAS:SetScoreOnFail(PlayerName,Penalty,TaskUnit) -self:F({PlayerName,Penalty,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScore("Failed","The Close Air Support (CAS) has failed!",Penalty) -return self -end -end -do -TASK_A2A_DISPATCHER={ -ClassName="TASK_A2A_DISPATCHER", -Mission=nil, -Detection=nil, -Tasks={}, -SweepZones={}, -} -function TASK_A2A_DISPATCHER:New(Mission,SetGroup,Detection) -local self=BASE:Inherit(self,DETECTION_MANAGER:New(SetGroup,Detection)) -self.Detection=Detection -self.Mission=Mission -self.Detection:FilterCategories(Unit.Category.AIRPLANE,Unit.Category.HELICOPTER) -self.Detection:InitDetectRadar(true) -self.Detection:SetRefreshTimeInterval(30) -self:AddTransition("Started","Assign","Started") -self:__Start(5) -return self -end -function TASK_A2A_DISPATCHER:SetEngageRadius(EngageRadius) -self.Detection:SetFriendliesRange(EngageRadius or 100000) -return self -end -function TASK_A2A_DISPATCHER:EvaluateINTERCEPT(DetectedItem) -self:F({DetectedItem.ItemID}) -local DetectedSet=DetectedItem.Set -local DetectedZone=DetectedItem.Zone -if DetectedItem.IsDetected==true then -local TargetSetUnit=SET_UNIT:New() -TargetSetUnit:SetDatabase(DetectedSet) -TargetSetUnit:FilterOnce() -return TargetSetUnit -end -return nil -end -function TASK_A2A_DISPATCHER:EvaluateSWEEP(DetectedItem) -self:F({DetectedItem.ItemID}) -local DetectedSet=DetectedItem.Set -local DetectedZone=DetectedItem.Zone -if DetectedItem.IsDetected==false then -local TargetSetUnit=SET_UNIT:New() -TargetSetUnit:SetDatabase(DetectedSet) -TargetSetUnit:FilterOnce() -return TargetSetUnit -end -return nil -end -function TASK_A2A_DISPATCHER:EvaluateENGAGE(DetectedItem) -self:F({DetectedItem.ItemID}) -local DetectedSet=DetectedItem.Set -local DetectedZone=DetectedItem.Zone -local PlayersCount,PlayersReport=self:GetPlayerFriendliesNearBy(DetectedItem) -if PlayersCount>0 and DetectedItem.IsDetected==true then -local TargetSetUnit=SET_UNIT:New() -TargetSetUnit:SetDatabase(DetectedSet) -TargetSetUnit:FilterOnce() -return TargetSetUnit -end -return nil -end -function TASK_A2A_DISPATCHER:EvaluateRemoveTask(Mission,Task,Detection,DetectedItem,DetectedItemIndex,DetectedItemChanged) -if Task then -if Task:IsStatePlanned()then -local TaskName=Task:GetName() -local TaskType=TaskName:match("(%u+)%.%d+") -self:T2({TaskType=TaskType}) -local Remove=false -local IsPlayers=Detection:IsPlayersNearBy(DetectedItem) -if TaskType=="ENGAGE"then -if IsPlayers==false then -Remove=true -end -end -if TaskType=="INTERCEPT"then -if IsPlayers==true then -Remove=true -end -if DetectedItem.IsDetected==false then -Remove=true -end -end -if TaskType=="SWEEP"then -if DetectedItem.IsDetected==true then -Remove=true -end -end -local DetectedSet=DetectedItem.Set -if DetectedSet:Count()==0 then -Remove=true -end -if DetectedItemChanged==true or Remove then -Task=self:RemoveTask(DetectedItemIndex) -end -end -end -return Task -end -function TASK_A2A_DISPATCHER:GetFriendliesNearBy(DetectedItem) -local DetectedSet=DetectedItem.Set -local FriendlyUnitsNearBy=self.Detection:GetFriendliesNearBy(DetectedItem) -local FriendlyTypes={} -local FriendliesCount=0 -if FriendlyUnitsNearBy then -local DetectedTreatLevel=DetectedSet:CalculateThreatLevelA2G() -for FriendlyUnitName,FriendlyUnitData in pairs(FriendlyUnitsNearBy)do -local FriendlyUnit=FriendlyUnitData -if FriendlyUnit:IsAirPlane()then -local FriendlyUnitThreatLevel=FriendlyUnit:GetThreatLevel() -FriendliesCount=FriendliesCount+1 -local FriendlyType=FriendlyUnit:GetTypeName() -FriendlyTypes[FriendlyType]=FriendlyTypes[FriendlyType]and(FriendlyTypes[FriendlyType]+1)or 1 -if DetectedTreatLevel0 then -for FriendlyType,FriendlyTypeCount in pairs(FriendlyTypes)do -FriendlyTypesReport:Add(string.format("%d of %s",FriendlyTypeCount,FriendlyType)) -end -else -FriendlyTypesReport:Add("-") -end -return FriendliesCount,FriendlyTypesReport -end -function TASK_A2A_DISPATCHER:GetPlayerFriendliesNearBy(DetectedItem) -local DetectedSet=DetectedItem.Set -local PlayersNearBy=self.Detection:GetPlayersNearBy(DetectedItem) -local PlayerTypes={} -local PlayersCount=0 -if PlayersNearBy then -local DetectedTreatLevel=DetectedSet:CalculateThreatLevelA2G() -for PlayerUnitName,PlayerUnitData in pairs(PlayersNearBy)do -local PlayerUnit=PlayerUnitData -local PlayerName=PlayerUnit:GetPlayerName() -if PlayerUnit:IsAirPlane()and PlayerName~=nil then -local FriendlyUnitThreatLevel=PlayerUnit:GetThreatLevel() -PlayersCount=PlayersCount+1 -local PlayerType=PlayerUnit:GetTypeName() -PlayerTypes[PlayerName]=PlayerType -if DetectedTreatLevel0 then -for PlayerName,PlayerType in pairs(PlayerTypes)do -PlayerTypesReport:Add(string.format('"%s" in %s',PlayerName,PlayerType)) -end -else -PlayerTypesReport:Add("-") -end -return PlayersCount,PlayerTypesReport -end -function TASK_A2A_DISPATCHER:RemoveTask(TaskIndex) -self.Mission:RemoveTask(self.Tasks[TaskIndex]) -self.Tasks[TaskIndex]=nil -end -function TASK_A2A_DISPATCHER:ProcessDetected(Detection) -self:E() -local AreaMsg={} -local TaskMsg={} -local ChangeMsg={} -local Mission=self.Mission -if Mission:IsIDLE()or Mission:IsENGAGED()then -local TaskReport=REPORT:New() -for TaskIndex,TaskData in pairs(self.Tasks)do -local Task=TaskData -if Task:IsStatePlanned()then -local DetectedItem=Detection:GetDetectedItem(TaskIndex) -if not DetectedItem then -local TaskText=Task:GetName() -for TaskGroupID,TaskGroup in pairs(self.SetGroup:GetSet())do -Mission:GetCommandCenter():MessageToGroup(string.format("Obsolete A2A task %s for %s removed.",TaskText,Mission:GetName()),TaskGroup) -end -Task=self:RemoveTask(TaskIndex) -end -end -end -for DetectedItemID,DetectedItem in pairs(Detection:GetDetectedItems())do -local DetectedItem=DetectedItem -local DetectedSet=DetectedItem.Set -local DetectedCount=DetectedSet:Count() -local DetectedZone=DetectedItem.Zone -local DetectedID=DetectedItem.ID -local TaskIndex=DetectedItem.Index -local DetectedItemChanged=DetectedItem.Changed -local Task=self.Tasks[TaskIndex] -Task=self:EvaluateRemoveTask(Mission,Task,Detection,DetectedItem,TaskIndex,DetectedItemChanged) -if not Task and DetectedCount>0 then -local TargetSetUnit=self:EvaluateENGAGE(DetectedItem) -if TargetSetUnit then -Task=TASK_A2A_ENGAGE:New(Mission,self.SetGroup,string.format("ENGAGE.%03d",DetectedID),TargetSetUnit) -Task:SetDetection(Detection,TaskIndex) -else -local TargetSetUnit=self:EvaluateINTERCEPT(DetectedItem) -if TargetSetUnit then -Task=TASK_A2A_INTERCEPT:New(Mission,self.SetGroup,string.format("INTERCEPT.%03d",DetectedID),TargetSetUnit) -Task:SetDetection(Detection,TaskIndex) -else -local TargetSetUnit=self:EvaluateSWEEP(DetectedItem) -if TargetSetUnit then -Task=TASK_A2A_SWEEP:New(Mission,self.SetGroup,string.format("SWEEP.%03d",DetectedID),TargetSetUnit) -Task:SetDetection(Detection,TaskIndex) -end -end -end -if Task then -self.Tasks[TaskIndex]=Task -Task:SetTargetZone(DetectedZone,DetectedItem.Coordinate.y,DetectedItem.Coordinate.Heading) -Task:SetDispatcher(self) -Mission:AddTask(Task) -TaskReport:Add(Task:GetName()) -else -self:E("This should not happen") -end -end -if Task then -local FriendliesCount,FriendliesReport=self:GetFriendliesNearBy(DetectedItem) -Task:SetInfo("Friendlies",string.format("%d ( %s )",FriendliesCount,FriendliesReport:Text(",")),30) -local PlayersCount,PlayersReport=self:GetPlayerFriendliesNearBy(DetectedItem) -Task:SetInfo("Players",string.format("%d ( %s )",PlayersCount,PlayersReport:Text(",")),31) -end -Detection:AcceptChanges(DetectedItem) -end -Mission:GetCommandCenter():SetMenu() -local TaskText=TaskReport:Text(", ") -for TaskGroupID,TaskGroup in pairs(self.SetGroup:GetSet())do -if(not Mission:IsGroupAssigned(TaskGroup))and TaskText~=""then -Mission:GetCommandCenter():MessageToGroup(string.format("%s has tasks %s. Subscribe to a task using the radio menu.",Mission:GetName(),TaskText),TaskGroup) -end -end -end -return true -end -end -do -TASK_A2A={ -ClassName="TASK_A2A", -} -function TASK_A2A:New(Mission,SetAttack,TaskName,TargetSetUnit,TaskType,TaskBriefing) -local self=BASE:Inherit(self,TASK:New(Mission,SetAttack,TaskName,TaskType,TaskBriefing)) -self:F() -self.TargetSetUnit=TargetSetUnit -self.TaskType=TaskType -local Fsm=self:GetUnitProcess() -Fsm:AddProcess("Planned","Accept",ACT_ASSIGN_ACCEPT:New(self.TaskBriefing),{Assigned="RouteToRendezVous",Rejected="Reject"}) -Fsm:AddTransition("Assigned","RouteToRendezVous","RoutingToRendezVous") -Fsm:AddProcess("RoutingToRendezVous","RouteToRendezVousPoint",ACT_ROUTE_POINT:New(),{Arrived="ArriveAtRendezVous"}) -Fsm:AddProcess("RoutingToRendezVous","RouteToRendezVousZone",ACT_ROUTE_ZONE:New(),{Arrived="ArriveAtRendezVous"}) -Fsm:AddTransition({"Arrived","RoutingToRendezVous"},"ArriveAtRendezVous","ArrivedAtRendezVous") -Fsm:AddTransition({"ArrivedAtRendezVous","HoldingAtRendezVous"},"Engage","Engaging") -Fsm:AddTransition({"ArrivedAtRendezVous","HoldingAtRendezVous"},"HoldAtRendezVous","HoldingAtRendezVous") -Fsm:AddProcess("Engaging","Account",ACT_ACCOUNT_DEADS:New(),{}) -Fsm:AddTransition("Engaging","RouteToTarget","Engaging") -Fsm:AddProcess("Engaging","RouteToTargetZone",ACT_ROUTE_ZONE:New(),{}) -Fsm:AddProcess("Engaging","RouteToTargetPoint",ACT_ROUTE_POINT:New(),{}) -Fsm:AddTransition("Engaging","RouteToTargets","Engaging") -Fsm:AddTransition("Rejected","Reject","Aborted") -Fsm:AddTransition("Failed","Fail","Failed") -function Fsm:onafterRouteToRendezVous(TaskUnit,Task) -self:E({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) -if Task:GetRendezVousZone(TaskUnit)then -self:__RouteToRendezVousZone(0.1) -else -if Task:GetRendezVousCoordinate(TaskUnit)then -self:__RouteToRendezVousPoint(0.1) -else -self:__ArriveAtRendezVous(0.1) -end -end -end -function Fsm:OnAfterArriveAtRendezVous(TaskUnit,Task) -self:E({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) -self:__Engage(0.1) -end -function Fsm:onafterEngage(TaskUnit,Task) -self:E({self}) -self:__Account(0.1) -self:__RouteToTarget(0.1) -self:__RouteToTargets(-10) -end -function Fsm:onafterRouteToTarget(TaskUnit,Task) -self:E({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) -if Task:GetTargetZone(TaskUnit)then -self:__RouteToTargetZone(0.1) -else -local TargetUnit=Task.TargetSetUnit:GetFirst() -if TargetUnit then -local Coordinate=TargetUnit:GetCoordinate() -self:T({TargetCoordinate=Coordinate,Coordinate:GetX(),Coordinate:GetAlt(),Coordinate:GetZ()}) -Task:SetTargetCoordinate(TargetUnit:GetCoordinate(),TaskUnit) -end -self:__RouteToTargetPoint(0.1) -end -end -function Fsm:onafterRouteToTargets(TaskUnit,Task) -self:E({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) -local TargetUnit=Task.TargetSetUnit:GetFirst() -if TargetUnit then -Task:SetTargetCoordinate(TargetUnit:GetCoordinate(),TaskUnit) -end -self:__RouteToTargets(-10) -end -return self -end -function TASK_A2A:GetPlannedMenuText() -return self:GetStateString().." - "..self:GetTaskName().." ( "..self.TargetSetUnit:GetUnitTypesText().." )" -end -function TASK_A2A:SetRendezVousCoordinate(RendezVousCoordinate,RendezVousRange,TaskUnit) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -local ActRouteRendezVous=ProcessUnit:GetProcess("RoutingToRendezVous","RouteToRendezVousPoint") -ActRouteRendezVous:SetCoordinate(RendezVousCoordinate) -ActRouteRendezVous:SetRange(RendezVousRange) -end -function TASK_A2A:GetRendezVousCoordinate(TaskUnit) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -local ActRouteRendezVous=ProcessUnit:GetProcess("RoutingToRendezVous","RouteToRendezVousPoint") -return ActRouteRendezVous:GetCoordinate(),ActRouteRendezVous:GetRange() -end -function TASK_A2A:SetRendezVousZone(RendezVousZone,TaskUnit) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -local ActRouteRendezVous=ProcessUnit:GetProcess("RoutingToRendezVous","RouteToRendezVousZone") -ActRouteRendezVous:SetZone(RendezVousZone) -end -function TASK_A2A:GetRendezVousZone(TaskUnit) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -local ActRouteRendezVous=ProcessUnit:GetProcess("RoutingToRendezVous","RouteToRendezVousZone") -return ActRouteRendezVous:GetZone() -end -function TASK_A2A:SetTargetCoordinate(TargetCoordinate,TaskUnit) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -local ActRouteTarget=ProcessUnit:GetProcess("Engaging","RouteToTargetPoint") -ActRouteTarget:SetCoordinate(TargetCoordinate) -end -function TASK_A2A:GetTargetCoordinate(TaskUnit) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -local ActRouteTarget=ProcessUnit:GetProcess("Engaging","RouteToTargetPoint") -return ActRouteTarget:GetCoordinate() -end -function TASK_A2A:SetTargetZone(TargetZone,Altitude,Heading,TaskUnit) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -local ActRouteTarget=ProcessUnit:GetProcess("Engaging","RouteToTargetZone") -ActRouteTarget:SetZone(TargetZone,Altitude,Heading) -end -function TASK_A2A:GetTargetZone(TaskUnit) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -local ActRouteTarget=ProcessUnit:GetProcess("Engaging","RouteToTargetZone") -return ActRouteTarget:GetZone() -end -function TASK_A2A:SetGoalTotal() -self.GoalTotal=self.TargetSetUnit:Count() -end -function TASK_A2A:GetGoalTotal() -return self.GoalTotal -end -end -do -TASK_A2A_INTERCEPT={ -ClassName="TASK_A2A_INTERCEPT", -} -function TASK_A2A_INTERCEPT:New(Mission,SetGroup,TaskName,TargetSetUnit,TaskBriefing) -local self=BASE:Inherit(self,TASK_A2A:New(Mission,SetGroup,TaskName,TargetSetUnit,"INTERCEPT",TaskBriefing)) -self:F() -Mission:AddTask(self) -self:SetBriefing( -TaskBriefing or -"Intercept incoming intruders.\n" -) -self:UpdateTaskInfo() -return self -end -function TASK_A2A_INTERCEPT:UpdateTaskInfo() -local TargetCoordinate=self.Detection and self.Detection:GetDetectedItemCoordinate(self.DetectedItemIndex)or self.TargetSetUnit:GetFirst():GetCoordinate() -self:SetInfo("Coordinates",TargetCoordinate,0) -self:SetInfo("Threat","["..string.rep("■",self.Detection and self.Detection:GetDetectedItemThreatLevel(self.DetectedItemIndex)or self.TargetSetUnit:CalculateThreatLevelA2G()).."]",11) -if self.Detection then -local DetectedItemsCount=self.TargetSetUnit:Count() -local ReportTypes=REPORT:New() -local TargetTypes={} -for TargetUnitName,TargetUnit in pairs(self.TargetSetUnit:GetSet())do -local TargetType=self.Detection:GetDetectedUnitTypeName(TargetUnit) -if not TargetTypes[TargetType]then -TargetTypes[TargetType]=TargetType -ReportTypes:Add(TargetType) -end -end -self:SetInfo("Targets",string.format("%d of %s",DetectedItemsCount,ReportTypes:Text(", ")),10) -else -local DetectedItemsCount=self.TargetSetUnit:Count() -local DetectedItemsTypes=self.TargetSetUnit:GetTypeNames() -self:SetInfo("Targets",string.format("%d of %s",DetectedItemsCount,DetectedItemsTypes),10) -end -end -function TASK_A2A_INTERCEPT:ReportOrder(ReportGroup) -self:F({TaskInfo=self.TaskInfo}) -local Coordinate=self.TaskInfo.Coordinates.TaskInfoText -local Distance=ReportGroup:GetCoordinate():Get2DDistance(Coordinate) -return Distance -end -function TASK_A2A_INTERCEPT:onafterGoal(TaskUnit,From,Event,To) -local TargetSetUnit=self.TargetSetUnit -if TargetSetUnit:Count()==0 then -self:Success() -end -self:__Goal(-10) -end -function TASK_A2A_INTERCEPT:SetScoreOnProgress(PlayerName,Score,TaskUnit) -self:F({PlayerName,Score,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScoreProcess("Engaging","Account","AccountForPlayer","Player "..PlayerName.." has intercepted a target.",Score) -return self -end -function TASK_A2A_INTERCEPT:SetScoreOnSuccess(PlayerName,Score,TaskUnit) -self:F({PlayerName,Score,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScore("Success","All targets have been successfully intercepted!",Score) -return self -end -function TASK_A2A_INTERCEPT:SetScoreOnFail(PlayerName,Penalty,TaskUnit) -self:F({PlayerName,Penalty,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScore("Failed","The intercept has failed!",Penalty) -return self -end -end -do -TASK_A2A_SWEEP={ -ClassName="TASK_A2A_SWEEP", -} -function TASK_A2A_SWEEP:New(Mission,SetGroup,TaskName,TargetSetUnit,TaskBriefing) -local self=BASE:Inherit(self,TASK_A2A:New(Mission,SetGroup,TaskName,TargetSetUnit,"SWEEP",TaskBriefing)) -self:F() -Mission:AddTask(self) -self:SetBriefing( -TaskBriefing or -"Perform a fighter sweep. Incoming intruders were detected and could be hiding at the location.\n" -) -self:UpdateTaskInfo() -return self -end -function TASK_A2A_SWEEP:UpdateTaskInfo() -local TargetCoordinate=self.Detection and self.Detection:GetDetectedItemCoordinate(self.DetectedItemIndex)or self.TargetSetUnit:GetFirst():GetCoordinate() -self:SetInfo("Coordinates",TargetCoordinate,0) -self:SetInfo("Assumed Threat","["..string.rep("■",self.Detection and self.Detection:GetDetectedItemThreatLevel(self.DetectedItemIndex)or self.TargetSetUnit:CalculateThreatLevelA2G()).."]",11) -if self.Detection then -local DetectedItemsCount=self.TargetSetUnit:Count() -local ReportTypes=REPORT:New() -local TargetTypes={} -for TargetUnitName,TargetUnit in pairs(self.TargetSetUnit:GetSet())do -local TargetType=self.Detection:GetDetectedUnitTypeName(TargetUnit) -if not TargetTypes[TargetType]then -TargetTypes[TargetType]=TargetType -ReportTypes:Add(TargetType) -end -end -self:SetInfo("Lost Targets",string.format("%d of %s",DetectedItemsCount,ReportTypes:Text(", ")),10) -else -local DetectedItemsCount=self.TargetSetUnit:Count() -local DetectedItemsTypes=self.TargetSetUnit:GetTypeNames() -self:SetInfo("Lost Targets",string.format("%d of %s",DetectedItemsCount,DetectedItemsTypes),10) -end -end -function TASK_A2A_SWEEP:ReportOrder(ReportGroup) -local Coordinate=self.TaskInfo.Coordinates.TaskInfoText -local Distance=ReportGroup:GetCoordinate():Get2DDistance(Coordinate) -return Distance -end -function TASK_A2A_SWEEP:onafterGoal(TaskUnit,From,Event,To) -local TargetSetUnit=self.TargetSetUnit -if TargetSetUnit:Count()==0 then -self:Success() -end -self:__Goal(-10) -end -function TASK_A2A_SWEEP:SetScoreOnProgress(PlayerName,Score,TaskUnit) -self:F({PlayerName,Score,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScoreProcess("Engaging","Account","AccountForPlayer","Player "..PlayerName.." has sweeped a target.",Score) -return self -end -function TASK_A2A_SWEEP:SetScoreOnSuccess(PlayerName,Score,TaskUnit) -self:F({PlayerName,Score,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScore("Success","All targets have been successfully sweeped!",Score) -return self -end -function TASK_A2A_SWEEP:SetScoreOnFail(PlayerName,Penalty,TaskUnit) -self:F({PlayerName,Penalty,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScore("Failed","The sweep has failed!",Penalty) -return self -end -end -do -TASK_A2A_ENGAGE={ -ClassName="TASK_A2A_ENGAGE", -} -function TASK_A2A_ENGAGE:New(Mission,SetGroup,TaskName,TargetSetUnit,TaskBriefing) -local self=BASE:Inherit(self,TASK_A2A:New(Mission,SetGroup,TaskName,TargetSetUnit,"ENGAGE",TaskBriefing)) -self:F() -Mission:AddTask(self) -self:SetBriefing( -TaskBriefing or -"Bogeys are nearby! Players close by are ordered to ENGAGE the intruders!\n" -) -self:UpdateTaskInfo() -return self -end -function TASK_A2A_ENGAGE:UpdateTaskInfo() -local TargetCoordinate=self.Detection and self.Detection:GetDetectedItemCoordinate(self.DetectedItemIndex)or self.TargetSetUnit:GetFirst():GetCoordinate() -self:SetInfo("Coordinates",TargetCoordinate,0) -self:SetInfo("Threat","["..string.rep("■",self.Detection and self.Detection:GetDetectedItemThreatLevel(self.DetectedItemIndex)or self.TargetSetUnit:CalculateThreatLevelA2G()).."]",11) -if self.Detection then -local DetectedItemsCount=self.TargetSetUnit:Count() -local ReportTypes=REPORT:New() -local TargetTypes={} -for TargetUnitName,TargetUnit in pairs(self.TargetSetUnit:GetSet())do -local TargetType=self.Detection:GetDetectedUnitTypeName(TargetUnit) -if not TargetTypes[TargetType]then -TargetTypes[TargetType]=TargetType -ReportTypes:Add(TargetType) -end -end -self:SetInfo("Targets",string.format("%d of %s",DetectedItemsCount,ReportTypes:Text(", ")),10) -else -local DetectedItemsCount=self.TargetSetUnit:Count() -local DetectedItemsTypes=self.TargetSetUnit:GetTypeNames() -self:SetInfo("Targets",string.format("%d of %s",DetectedItemsCount,DetectedItemsTypes),10) -end -end -function TASK_A2A_ENGAGE:ReportOrder(ReportGroup) -local Coordinate=self.TaskInfo.Coordinates.TaskInfoText -local Distance=ReportGroup:GetCoordinate():Get2DDistance(Coordinate) -return Distance -end -function TASK_A2A_ENGAGE:onafterGoal(TaskUnit,From,Event,To) -local TargetSetUnit=self.TargetSetUnit -if TargetSetUnit:Count()==0 then -self:Success() -end -self:__Goal(-10) -end -function TASK_A2A_ENGAGE:SetScoreOnProgress(PlayerName,Score,TaskUnit) -self:F({PlayerName,Score,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScoreProcess("Engaging","Account","AccountForPlayer","Player "..PlayerName.." has engaged and destroyed a target.",Score) -return self -end -function TASK_A2A_ENGAGE:SetScoreOnSuccess(PlayerName,Score,TaskUnit) -self:F({PlayerName,Score,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScore("Success","All targets have been successfully engaged!",Score) -return self -end -function TASK_A2A_ENGAGE:SetScoreOnFail(PlayerName,Penalty,TaskUnit) -self:F({PlayerName,Penalty,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScore("Failed","The target engagement has failed!",Penalty) -return self -end -end -do -TASK_CARGO={ -ClassName="TASK_CARGO", -} -function TASK_CARGO:New(Mission,SetGroup,TaskName,SetCargo,TaskType,TaskBriefing) -local self=BASE:Inherit(self,TASK:New(Mission,SetGroup,TaskName,TaskType,TaskBriefing)) -self:F({Mission,SetGroup,TaskName,SetCargo,TaskType}) -self.SetCargo=SetCargo -self.TaskType=TaskType -self.SmokeColor=SMOKECOLOR.Red -self.CargoItemCount={} -self.CargoLimit=2 -self.DeployZones={} -local Fsm=self:GetUnitProcess() -Fsm:SetStartState("Planned") -Fsm:AddProcess("Planned","Accept",ACT_ASSIGN_ACCEPT:New(self.TaskBriefing),{Assigned="SelectAction",Rejected="Reject"}) -Fsm:AddTransition({"Assigned","WaitingForCommand","ArrivedAtPickup","ArrivedAtDeploy","Boarded","UnBoarded","Landed","Boarding"},"SelectAction","*") -Fsm:AddTransition("*","RouteToPickup","RoutingToPickup") -Fsm:AddProcess("RoutingToPickup","RouteToPickupPoint",ACT_ROUTE_POINT:New(),{Arrived="ArriveAtPickup",Cancelled="CancelRouteToPickup"}) -Fsm:AddTransition("Arrived","ArriveAtPickup","ArrivedAtPickup") -Fsm:AddTransition("Cancelled","CancelRouteToPickup","WaitingForCommand") -Fsm:AddTransition("*","RouteToDeploy","RoutingToDeploy") -Fsm:AddProcess("RoutingToDeploy","RouteToDeployZone",ACT_ROUTE_ZONE:New(),{Arrived="ArriveAtDeploy",Cancelled="CancelRouteToDeploy"}) -Fsm:AddTransition("Arrived","ArriveAtDeploy","ArrivedAtDeploy") -Fsm:AddTransition("Cancelled","CancelRouteToDeploy","WaitingForCommand") -Fsm:AddTransition({"ArrivedAtPickup","ArrivedAtDeploy","Landing"},"Land","Landing") -Fsm:AddTransition("Landing","Landed","Landed") -Fsm:AddTransition("*","PrepareBoarding","AwaitBoarding") -Fsm:AddTransition("AwaitBoarding","Board","Boarding") -Fsm:AddTransition("Boarding","Boarded","Boarded") -Fsm:AddTransition("*","PrepareUnBoarding","AwaitUnBoarding") -Fsm:AddTransition("AwaitUnBoarding","UnBoard","UnBoarding") -Fsm:AddTransition("UnBoarding","UnBoarded","UnBoarded") -Fsm:AddTransition("Deployed","Success","Success") -Fsm:AddTransition("Rejected","Reject","Aborted") -Fsm:AddTransition("Failed","Fail","Failed") -function Fsm:onafterSelectAction(TaskUnit,Task) -local TaskUnitName=TaskUnit:GetName() -self:E({TaskUnit=TaskUnitName,Task=Task and Task:GetClassNameAndID()}) -local MenuTime=timer.getTime() -TaskUnit.Menu=MENU_GROUP:New(TaskUnit:GetGroup(),Task:GetName().." @ "..TaskUnit:GetName()):SetTime(MenuTime) -local CargoItemCount=TaskUnit:CargoItemCount() -Task.SetCargo:ForEachCargo( -function(Cargo) -if Cargo:IsAlive()then -if Cargo:IsUnLoaded()then -if CargoItemCount0 and SmokeColor<=5 then -self.SmokeColor=SMOKECOLOR.SmokeColor -end -end -end -function TASK_CARGO:GetSmokeColor() -return self.SmokeColor -end -function TASK_CARGO:GetPlannedMenuText() -return self:GetStateString().." - "..self:GetTaskName().." ( "..self.TargetSetUnit:GetUnitTypesText().." )" -end -function TASK_CARGO:GetCargoSet() -return self.SetCargo -end -function TASK_CARGO:GetDeployZones() -return self.DeployZones -end -function TASK_CARGO:SetCargoPickup(Cargo,TaskUnit) -self:F({Cargo,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -local ActRouteCargo=ProcessUnit:GetProcess("RoutingToPickup","RouteToPickupPoint") -ActRouteCargo:Reset() -ActRouteCargo:SetCoordinate(Cargo:GetCoordinate()) -ActRouteCargo:SetRange(Cargo:GetBoardingRange()) -ActRouteCargo:SetMenuCancel(TaskUnit:GetGroup(),"Cancel Routing to Cargo "..Cargo:GetName(),TaskUnit.Menu) -ActRouteCargo:Start() -return self -end -function TASK_CARGO:SetDeployZone(DeployZone,TaskUnit) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -local ActRouteDeployZone=ProcessUnit:GetProcess("RoutingToDeploy","RouteToDeployZone") -ActRouteDeployZone:Reset() -ActRouteDeployZone:SetZone(DeployZone) -ActRouteDeployZone:SetMenuCancel(TaskUnit:GetGroup(),"Cancel Routing to Deploy Zone"..DeployZone:GetName(),TaskUnit.Menu) -ActRouteDeployZone:Start() -return self -end -function TASK_CARGO:AddDeployZone(DeployZone,TaskUnit) -self.DeployZones[DeployZone:GetName()]=DeployZone -return self -end -function TASK_CARGO:RemoveDeployZone(DeployZone,TaskUnit) -self.DeployZones[DeployZone:GetName()]=nil -return self -end -function TASK_CARGO:SetDeployZones(DeployZones,TaskUnit) -for DeployZoneID,DeployZone in pairs(DeployZones)do -self.DeployZones[DeployZone:GetName()]=DeployZone -end -return self -end -function TASK_CARGO:GetTargetZone(TaskUnit) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -local ActRouteTarget=ProcessUnit:GetProcess("Engaging","RouteToTargetZone") -return ActRouteTarget:GetZone() -end -function TASK_CARGO:SetScoreOnProgress(Text,Score,TaskUnit) -self:F({Text,Score,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScoreProcess("Engaging","Account","Account",Text,Score) -return self -end -function TASK_CARGO:SetScoreOnSuccess(Text,Score,TaskUnit) -self:F({Text,Score,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScore("Success",Text,Score) -return self -end -function TASK_CARGO:SetScoreOnFail(Text,Penalty,TaskUnit) -self:F({Text,Score,TaskUnit}) -local ProcessUnit=self:GetUnitProcess(TaskUnit) -ProcessUnit:AddScore("Failed",Text,Penalty) -return self -end -function TASK_CARGO:SetGoalTotal() -self.GoalTotal=self.SetCargo:Count() -end -function TASK_CARGO:GetGoalTotal() -return self.GoalTotal -end -end -do -TASK_CARGO_TRANSPORT={ -ClassName="TASK_CARGO_TRANSPORT", -} -function TASK_CARGO_TRANSPORT:New(Mission,SetGroup,TaskName,SetCargo,TaskBriefing) -local self=BASE:Inherit(self,TASK_CARGO:New(Mission,SetGroup,TaskName,SetCargo,"Transport",TaskBriefing)) -self:F() -Mission:AddTask(self) -self:AddTransition("*","CargoPickedUp","*") -self:AddTransition("*","CargoDeployed","*") -self:E({CargoDeployed=self.CargoDeployed~=nil and"true"or"false"}) -local Fsm=self:GetUnitProcess() -local CargoReport=REPORT:New("Transport Cargo. The following cargo needs to be transported including initial positions:") -SetCargo:ForEachCargo( -function(Cargo) -local CargoType=Cargo:GetType() -local CargoName=Cargo:GetName() -local CargoCoordinate=Cargo:GetCoordinate() -CargoReport:Add(string.format('- "%s" (%s) at %s',CargoName,CargoType,CargoCoordinate:ToStringMGRS())) -end -) -self:SetBriefing( -TaskBriefing or -CargoReport:Text() -) -return self -end -function TASK_CARGO_TRANSPORT:ReportOrder(ReportGroup) -return true -end -function TASK_CARGO_TRANSPORT:IsAllCargoTransported() -local CargoSet=self:GetCargoSet() -local Set=CargoSet:GetSet() -local DeployZones=self:GetDeployZones() -local CargoDeployed=true -for CargoID,CargoData in pairs(Set)do -local Cargo=CargoData -if Cargo:IsDeployed()then -for DeployZoneID,DeployZone in pairs(DeployZones)do -self:T({Cargo.CargoObject}) -if Cargo:IsInZone(DeployZone)==false then -CargoDeployed=false -end -end -else -CargoDeployed=false -end -end -return CargoDeployed -end -function TASK_CARGO_TRANSPORT:onafterGoal(TaskUnit,From,Event,To) -local CargoSet=self.CargoSet -if self:IsAllCargoTransported()then -self:Success() -end -self:__Goal(-10) -end -end -_EVENTDISPATCHER=EVENT:New() -_SCHEDULEDISPATCHER=SCHEDULEDISPATCHER:New() -_DATABASE=DATABASE:New() -_SETTINGS=SETTINGS:Set() -BASE:TraceOnOff(false) +env.info('*** MOOSE DYNAMIC INCLUDE START *** ') +env.info('Moose Generation Timestamp: 20171006_1300') +local base=_G +__Moose={} +__Moose.Include=function(IncludeFile) +if not __Moose.Includes[IncludeFile]then +__Moose.Includes[IncludeFile]=IncludeFile +local f=assert(base.loadfile(__Moose.ProgramPath..IncludeFile)) +if f==nil then +error("Moose: Could not load Moose file "..IncludeFile) +else +env.info("Moose: "..IncludeFile.." dynamically loaded from "..__Moose.ProgramPath) +return f() +end +end +end +__Moose.ProgramPath="Scripts/Moose/" +__Moose.Includes={} +__Moose.Include('Utilities/Routines.lua') +__Moose.Include('Utilities/Utils.lua') +__Moose.Include('Core/Base.lua') +__Moose.Include('Core/Report.lua') +__Moose.Include('Core/Scheduler.lua') +__Moose.Include('Core/ScheduleDispatcher.lua') +__Moose.Include('Core/Event.lua') +__Moose.Include('Core/Settings.lua') +__Moose.Include('Core/Menu.lua') +__Moose.Include('Core/Zone.lua') +__Moose.Include('Core/Database.lua') +__Moose.Include('Core/Set.lua') +__Moose.Include('Core/Point.lua') +__Moose.Include('Core/Message.lua') +__Moose.Include('Core/Fsm.lua') +__Moose.Include('Core/Radio.lua') +__Moose.Include('Core/SpawnStatic.lua') +__Moose.Include('Core/Cargo.lua') +__Moose.Include('Core/Spot.lua') +__Moose.Include('Wrapper/Object.lua') +__Moose.Include('Wrapper/Identifiable.lua') +__Moose.Include('Wrapper/Positionable.lua') +__Moose.Include('Wrapper/Controllable.lua') +__Moose.Include('Wrapper/Group.lua') +__Moose.Include('Wrapper/Unit.lua') +__Moose.Include('Wrapper/Client.lua') +__Moose.Include('Wrapper/Static.lua') +__Moose.Include('Wrapper/Airbase.lua') +__Moose.Include('Wrapper/Scenery.lua') +__Moose.Include('Functional/Scoring.lua') +__Moose.Include('Functional/CleanUp.lua') +__Moose.Include('Functional/Spawn.lua') +__Moose.Include('Functional/Movement.lua') +__Moose.Include('Functional/Sead.lua') +__Moose.Include('Functional/Escort.lua') +__Moose.Include('Functional/MissileTrainer.lua') +__Moose.Include('Functional/AirbasePolice.lua') +__Moose.Include('Functional/Detection.lua') +__Moose.Include('Functional/Designate.lua') +__Moose.Include('Functional/RAT.lua') +__Moose.Include('AI/AI_Balancer.lua') +__Moose.Include('AI/AI_A2A.lua') +__Moose.Include('AI/AI_A2A_Patrol.lua') +__Moose.Include('AI/AI_A2A_Cap.lua') +__Moose.Include('AI/AI_A2A_Gci.lua') +__Moose.Include('AI/AI_A2A_Dispatcher.lua') +__Moose.Include('AI/AI_Patrol.lua') +__Moose.Include('AI/AI_Cap.lua') +__Moose.Include('AI/AI_Cas.lua') +__Moose.Include('AI/AI_Bai.lua') +__Moose.Include('AI/AI_Formation.lua') +__Moose.Include('Actions/Act_Assign.lua') +__Moose.Include('Actions/Act_Route.lua') +__Moose.Include('Actions/Act_Account.lua') +__Moose.Include('Actions/Act_Assist.lua') +__Moose.Include('Tasking/CommandCenter.lua') +__Moose.Include('Tasking/Mission.lua') +__Moose.Include('Tasking/Task.lua') +__Moose.Include('Tasking/DetectionManager.lua') +__Moose.Include('Tasking/Task_A2G_Dispatcher.lua') +__Moose.Include('Tasking/Task_A2G.lua') +__Moose.Include('Tasking/Task_A2A_Dispatcher.lua') +__Moose.Include('Tasking/Task_A2A.lua') +__Moose.Include('Tasking/Task_Cargo.lua') +__Moose.Include('Moose.lua') +BASE:TraceOnOff(true) env.info('*** MOOSE INCLUDE END *** ')