diff --git a/Moose Development/Moose/Core/SpawnStatic.lua b/Moose Development/Moose/Core/SpawnStatic.lua index e5a7b4e59..66c3581d0 100644 --- a/Moose Development/Moose/Core/SpawnStatic.lua +++ b/Moose Development/Moose/Core/SpawnStatic.lua @@ -291,7 +291,7 @@ end -- @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. +-- @param #string NewName (optional) The name of the new static. -- @return #SPAWNSTATIC function SPAWNSTATIC:SpawnFromZone( Zone, Heading, NewName ) --R2.1 self:F( { Zone, Heading, NewName } ) diff --git a/Moose Development/Moose/Functional/FlightModelData.lua b/Moose Development/Moose/Functional/FlightModelData.lua new file mode 100644 index 000000000..03e0f60d1 --- /dev/null +++ b/Moose Development/Moose/Functional/FlightModelData.lua @@ -0,0 +1,544 @@ +--- **Functional** - (R2.5) - Monitor and export flight model data. +-- +-- +-- +-- RAT2 creates random air traffic on the map. +-- +-- +-- +-- **Main Features:** +-- +-- * It's very random. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Functional.FMData +-- @image Functional_Rat2.png + + +--- FMD class. +-- @type FMD +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @field #table players Player table. +-- @field #table menuadded Table of units where the F10 radio menu was added. +-- @field #string savepath Path to save data files. +-- @extends Core.Fsm#FSM + +--- Be surprised! +-- +-- === +-- +-- ![Banner Image](..\Presentations\RAT2\RAT2_Main.png) +-- +-- # The RAT2 Concept +-- +-- +-- +-- @field #FMD +FMD = { + ClassName = "FMD", + Debug = false, + lid = nil, + players = {}, + menuadded = {}, + savepath = nil, +} + +--- Player data table. +-- @type FMD.PlayerData +-- @field Core.Scheduler#SCHEDULER scheduler Scheduler +-- @field Wrapper.Unit#UNIT unit Player unit. +-- @field #string unitname Name of the unit. +-- @field #string actype Aircraft type. +-- @field #string name Player name. +-- @field #number dt Time step for data recording in seconds. Default 0.01 sec ==> 100 data points per second! +-- @field #table data Data table. +-- @field #number SID Scheduler ID. + +--- Data point table. +-- @type FMD.DataPoint +-- @field #number time Abs mission time. +-- @field #number T Temperature in degrees Celsius. +-- @field #number P Pressure. +-- @field #number Alt Altitude ASL in meters. +-- @field #number AoA Angle of Attack in degrees. +-- @field #number Pitch Pitch angle in degrees. +-- @field #number Roll Roll angle in degrees. +-- @field #number Yaw Yaw angle in degrees. +-- @field #number Climbrate Climb rate in m/s. +-- @field #number Vtot Total velocity in m/s. +-- @field DCS#Vec3 v Velocity vector. Components x,y,z in m/s. +-- @field DCS#Vec3 o Orientation vector. Components x,y,z in meters. +-- @field #number omega Angle velocity in m/s. +-- @field #number hdg Heading in degrees. +-- @field #number Atot Total acceleration m/s^2. +-- @field DCS#Vec3 a Accelleration vector. +-- @field #number DRoll Roll speed in degrees/second. +-- @field #number DPitch Pitch speed in degrees/second. +-- @field #number DYaw Yaw speed in degrees/second. + +--- Main group level radio menu: F10 Other/Airboss. +-- @field #table MenuF10 +FMD.MenuF10={} + +--- FMD mission level F10 root menu. +-- @field #table MenuF10Root +FMD.MenuF10Root=nil + +--- FMD class version. +-- @field #string version +FMD.version="0.0.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new FMD class object. +-- @param #FMD self +-- @return #FMD self. +function FMD:New() + + -- Inherit everthing from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #FMD + + -- Start State. + self:SetStartState("Stopped") + + -- Log string. + self.lid="FMD | " + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FMD script. + self:AddTransition("*", "Status", "*") -- Start FMD script. + + return self +end + +--- On after Start event. +-- @param #FMD self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FMD:onafterStart(From, Event, To) + + -- Handle events. + self:HandleEvent(EVENTS.Birth) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- FMD event handler for event birth. +-- @param #FMD self +-- @param Core.Event#EVENTDATA EventData +function FMD:OnEventBirth(EventData) + self:F3({eventbirth = EventData}) + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T2(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) + self:T2(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) + self:T2(self.lid.."BIRTH: player = "..tostring(_playername)) + + if _unit and _playername then + + local _uid=_unit:GetID() + local _group=_unit:GetGroup() + local _callsign=_unit:GetCallsign() + + -- Debug output. + local text=string.format("Pilot %s, callsign %s entered unit %s of group %s.", _playername, _callsign, _unitName, _group:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + + + -- Add Menu commands. + self:_AddF10Commands(_unitName) + + self.players[_playername]={} + + local player=self.players[_playername] --#FMD.PlayerData + + player.scheduler=SCHEDULER:New() + player.unit=_unit + player.unitname=_unitName + player.name=_playername + player.actype=_unit:GetTypeName() + player.dt=0.01 + player.data={} + + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Data Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get data of unit. +-- @param #FMD self +-- @param Wrapper.Unit#UNIT unit +-- @return #FMD.DataPoint Datapoint. +function FMD:_GetDataPoint(unit) + + -- Current coordinate. + local coord=unit:GetCoordinate() + + local dp={} --#FMD.DataPoint + + dp.a={} + dp.Alt=coord.y + dp.AoA=unit:GetAoA() + dp.Atot=nil + dp.P=coord:GetPressure() + dp.Pitch=unit:GetPitch() + dp.Roll=unit:GetRoll() + dp.time=timer.getAbsTime() + dp.T=coord:GetTemperature() + dp.v=unit:GetVelocityVec3() + dp.Vtot=UTILS.VecNorm(dp.v) + dp.Yaw=unit:GetYaw() + dp.o=unit:GetOrientation() + dp.Hdg=unit:GetHeading() + + return dp +end + +--- Get data of unit. +-- @param #FMD self +-- @param #FMD.PlayerData playerData Player data. +-- @return #FMD.DataPoint Datapoint. +function FMD:_Derivative(playerData) + + local function numderiv(fpm,fpp,h) + return 0.5*(fpp-fpm)/h + end + + self:E("derivative #"..#playerData.data) + + local datapoints=playerData.data + local dt=playerData.dt + + for i=2,#datapoints-1 do + + self:E("i="..i) + + local dpm=datapoints[i-1] --#FMD.DataPoint + local dpp=datapoints[i+1] --#FMD.DataPoint + local dpi=datapoints[i] --#FMD.DataPoint + + self:E(dpm) + self:E(dpp) + self:E(dpi) + + dpi.Atot=numderiv(dpm.Vtot, dpp.Vtot, dt) + + dpi.a.x=numderiv(dpm.v.x, dpp.v.x, dt) + dpi.a.y=numderiv(dpm.v.y, dpp.v.y, dt) + dpi.a.z=numderiv(dpm.v.z, dpp.v.z, dt) + + dpi.DPitch=numderiv(dpm.Pitch, dpp.Pitch, dt) + dpi.DRoll=numderiv(dpm.Roll, dpp.Roll, dt) + dpi.DYaw=numderiv(dpm.Yaw, dpp.Yaw, dt) + + local ang=UTILS.VecAngle(dpm.o.x, dpp.o.x) + --local dvpp=UTILS.VecAngle(dpi.o.x, dpp.o.x) + + dpi.omega=numderiv(0, ang) + + end + +end + +--- Record data +-- @param #FMD self +-- @param #FMD.PlayerData playerData Player data table. +function FMD:_RecordData(playerData) + + local dp=self:_GetDataPoint(playerData.unit) + + table.insert(playerData.data, dp) + +end + +--- Save data. +-- @param #FMD self +-- @param #FMD.PlayerData playerData Player data table. +function FMD:_SaveData(playerData) + + -- Nothing to save. + if playerData==nil or #playerData.data==0 or not io then + return + end + + --- Function that saves data to file + local function _savefile(filename, data) + local f = assert(io.open(filename, "wb")) + f:write(data) + f:close() + end + + -- Set path or default. + local path=self.savepath + if lfs then + path=path or lfs.writedir() + end + + self:E("FF save data") + + -- Create unused file name. + local filename=nil + for i=1,9999 do + + -- Create file name. + filename=string.format("FMD-%s_%s-%04d.csv", playerData.name, playerData.actype, i) + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Check if file exists. + local _exists=UTILS.FileExists(filename) + if not _exists then + break + end + end + + + -- Info + local text=string.format("Saving player %s flight data to file %s", playerData.name, filename) + self:I(self.lid..text) + + -- Header line + local data="#Time,Altitude,Temperature,Pressure,Vtot,Vx,Vy,Vz,Atot,ax,ay,az,AoA,Pitch,dPitch/dt,Roll,dRoll/dt,Yaw,dYaw/dt\n" + + local g0=playerData.data[1] --#FMD.DataPoint + local T0=g0.time + + -- Calculate derivatives. + self:_Derivative(playerData) + + self:E("looping data.") + + for i=2,#playerData.data-1 do + + local dp=playerData.data[i] --#FMD.DataPoint + + self:E("i="..i) + self:E(dp) + + -- + local ms2mach=0.00291545 + + local t=(dp.time-T0) or 0 + local a=dp.Alt or 0 + local b=dp.T or 0 + local c=dp.P or 0 + local d=dp.Vtot or 0 + local e=dp.v.x or 0 + local f=dp.v.y or 0 + local g=dp.v.z or 0 + local h=dp.Atot or 0 + local i=dp.a.x or 0 + local j=dp.a.y or 0 + local k=dp.a.z or 0 + local l=dp.AoA or 0 + local m=dp.Pitch or 0 + local n=dp.DPitch or 0 + local o=dp.Roll or 0 + local p=dp.DRoll or 0 + local q=dp.Yaw or 0 + local r=dp.DYaw or 0 + local s=dp.omega or 0 + self:E(data) + self:E(t) + self:E(a) + self:E(b) + self:E(c) + self:E(d) + self:E(e) + self:E(f) + self:E(g) + self:E(h) + self:E(i) + self:E(j) + self:E(k) + self:E(l) + self:E(m) + self:E(n) + self:E(o) + self:E(p) + self:E(q) + self:E(r) + self:E(s) + --self:E(u) + -- t a b c d e f g h i j k l m n o p q r s + data=data..string.format("%.2f,%.2f,%.2f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f \n", + t, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) + --data=data..string.format("%.2f\n",t) + self:E(data) + end + + -- Save file. + _savefile(filename, data) + + -- Clear data. + playerData.data={} +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RADIO MENU Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add menu commands for player. +-- @param #FMD self +-- @param #string _unitName Name of player unit. +function FMD:_AddF10Commands(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check for player unit. + if _unit and playername then + + -- Get group and ID. + local group=_unit:GetGroup() + local gid=group:GetID() + + if group and gid then + + if not self.menuadded[gid] then + + -- Enable switch so we don't do this twice. + self.menuadded[gid]=true + + -- Set menu root path. + local _rootPath=nil + if FMD.MenuF10Root then + ------------------------ + -- MISSON LEVEL MENUE -- + ------------------------ + + -- F10/FMD/... + _rootPath=FMD.MenuF10Root + + else + ------------------------ + -- GROUP LEVEL MENUES -- + ------------------------ + + -- Main F10 menu: F10/FMD/ + if FMD.MenuF10[gid]==nil then + FMD.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "FMD") + end + + + -- F10/FMD/... + _rootPath=FMD.MenuF10[gid] + + end + + -------------------------------- + -- F10/FMD//F1 Help + -------------------------------- + + -- F10/FMD/Start Recording + missionCommands.addCommandForGroup(gid, "Start Recording", _rootPath, self._StartRecording, self, _unitName) -- F1 + missionCommands.addCommandForGroup(gid, "Stop Recording", _rootPath, self._StopRecording, self, _unitName) -- F2 + + end + end + end +end + +--- Start data recording. +-- @param #FMD self +-- @param #string _unitName Name of player unit. +function FMD:_StartRecording(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#FMD.PlayerData + + if playerData then + + local delay=3 + + MESSAGE:New(string.format("Flight data recording will be started in %d seconds. dt=%.3f sec", delay, playerData.dt)):ToAll() + + playerData.SID=playerData.scheduler:Schedule(nil, self._RecordData, {self, playerData}, delay, playerData.dt) + end + end + +end + +--- Stop data recording. +-- @param #FMD self +-- @param #string _unitName Name of player unit. +function FMD:_StopRecording(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#FMD.PlayerData + + if playerData then + + -- Stop scheduler. + playerData.scheduler:Stop(playerData.SID) + + -- Save data. + self:_SaveData(playerData) + + end + end + +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #FMD self +-- @param #string _unitName Name of the player unit. +-- @return Wrapper.Unit#UNIT Unit of player or nil. +-- @return #string Name of the player or nil. +function FMD:_GetPlayerUnitAndName(_unitName) + self:F2(_unitName) + + if _unitName ~= nil then + + -- Get DCS unit from its name. + local DCSunit=Unit.getByName(_unitName) + + if DCSunit then + + local playername=DCSunit:getPlayerName() + local unit=UNIT:Find(DCSunit) + + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) + if DCSunit and unit and playername then + return unit, playername + end + + end + + end + + -- Return nil if we could not find a player. + return nil,nil +end + + + + diff --git a/Moose Development/Moose/Functional/Fox2.lua b/Moose Development/Moose/Functional/Fox2.lua new file mode 100644 index 000000000..202d2e854 --- /dev/null +++ b/Moose Development/Moose/Functional/Fox2.lua @@ -0,0 +1,724 @@ +--- **Functional** - (R2.5) - Yet another missile trainer. +-- +-- +-- Train to evade missiles without being destroyed. +-- +-- +-- **Main Features:** +-- +-- * Adaptive update of missile-to-player distance. +-- * F10 radio menu. +-- * Easy to use. +-- * Handles air-to-air and surface-to-air missiles. +-- * Alert on missile launch (optional). +-- * Marker of missile launch position (optional). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Functional.FOX2 +-- @image Functional_FOX2.png + + +--- FOX2 class. +-- @type FOX2 +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @extends Core.Fsm#FSM + +--- Fox 2! +-- +-- === +-- +-- ![Banner Image](..\Presentations\FOX2\FOX2_Main.png) +-- +-- # The FOX2 Concept +-- +-- +-- +-- @field #FOX2 +FOX2 = { + ClassName = "FOX2", + Debug = false, + lid = nil, + menuadded = {}, + missiles = {}, + players = {}, + destroy = nil, + safezones = {}, + launchzones = {}, + exlosionpower = 5, +} + + +--- Player data table holding all important parameters of each player. +-- @type FOX2.PlayerData +-- @field Wrapper.Unit#UNIT unit Aircraft of the player. +-- @field #string unitname Name of the unit. +-- @field Wrapper.Client#CLIENT client Client object of player. +-- @field #string callsign Callsign of player. +-- @field Wrapper.Group#GROUP group Aircraft group of player. +-- @field #string groupname Name of the the player aircraft group. +-- @field #string name Player name. +-- @field #number coalition Coalition number of player. +-- @field #boolean destroy Destroy missile. +-- @field #boolean launchalert Alert player on detected missile launch. +-- @field #boolean marklaunch Mark position of launched missile on F10 map. +-- @field #number defeated Number of missiles defeated. +-- @field #number dead Number of missiles not defeated. + +--- Missile data +-- @type FOX2.MissileData +-- @field #string missiletype Type of missile. +-- @field Wrapper.Unit#UNIT shooterunit Unit that shot the missile. +-- @field Wrapper.Group#GROUP shootergroup Group that shot the missile. +-- @field #number shottime Abs mission time in seconds the missile was fired. +-- @field Wrapper.Unit#UNIT targetunit Unit that was targeted. + + +--- Main radio menu on group level. +-- @field #table MenuF10 Root menu table on group level. +FOX2.MenuF10={} + +--- Main radio menu on mission level. +-- @field #table MenuF10Root Root menu on mission level. +FOX2.MenuF10Root=nil + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO list: +-- TODO: safe zones +-- TODO: mark shooter on F10 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new FOX2 class object. +-- @param #FOX2 self +-- @return #FOX2 self. +function FOX2:New() + + self.lid="FOX2 | " + + -- Inherit everthing from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #FOX2 + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FOX2 script. + self:AddTransition("*", "Status", "*") -- Start FOX2 script. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the FOX2. Initializes parameters and starts event handlers. + -- @function [parent=#FOX2] Start + -- @param #FOX2 self + + --- Triggers the FSM event "Start" after a delay. Starts the FOX2. Initializes parameters and starts event handlers. + -- @function [parent=#FOX2] __Start + -- @param #FOX2 self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the FOX2 and all its event handlers. + -- @param #FOX2 self + + --- Triggers the FSM event "Stop" after a delay. Stops the FOX2 and all its event handlers. + -- @function [parent=#FOX2] __Stop + -- @param #FOX2 self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#FOX2] Status + -- @param #FOX2 self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#FOX2] __Status + -- @param #FOX2 self + -- @param #number delay Delay in seconds. + + return self +end + +--- On after Start event. Starts the missile trainer and adds event handlers. +-- @param #FOX2 self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FOX2:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting FOX2 Missile Trainer v0.0.1") + env.info(text) + + -- Handle events: + self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.Shot) + + self:TraceClass(self.ClassName) + self:TraceLevel(2) + + self:__Status(-1) +end + +--- On after Stop event. Stops the missile trainer and unhandles events. +-- @param #FOX2 self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FOX2:onafterStop(From, Event, To) + + -- Short info. + local text=string.format("Stopping FOX2 Missile Trainer v0.0.1") + env.info(text) + + -- Handle events: + self:UnhandleEvent(EVENTS.Birth) + self:UnhandleEvent(EVENTS.Shot) + +end + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status Functions +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check spawn queue and spawn aircraft if necessary. +-- @param #FOX2 self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FOX2:onafterStatus(From, Event, To) + + self:I(self.lid..string.format("Missile trainer status.")) + + self:__Status(-30) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- FOX2 event handler for event birth. +-- @param #FOX2 self +-- @param Core.Event#EVENTDATA EventData +function FOX2:OnEventBirth(EventData) + self:F3({eventbirth = EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event BIRTH!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event BIRTH!") + self:E(EventData) + return + end + + -- Player unit and name. + local _unitName=EventData.IniUnitName + local playerunit, playername=self:_GetPlayerUnitAndName(_unitName) + + -- Debug info. + self:T(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) + self:T(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) + self:T(self.lid.."BIRTH: player = "..tostring(playername)) + + -- Check if player entered. + if playerunit and playername then + + local _uid=playerunit:GetID() + local _group=playerunit:GetGroup() + local _callsign=playerunit:GetCallsign() + + -- Debug output. + local text=string.format("Pilot %s, callsign %s entered unit %s of group %s.", playername, _callsign, _unitName, _group:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + + -- Add Menu commands. + self:_AddF10Commands(_unitName) + + -- Player data. + local playerData={} --#FOX2.PlayerData + + -- Player unit, client and callsign. + playerData.unit = playerunit + playerData.unitname = _unitName + playerData.group = _group + playerData.groupname = _group:GetName() + playerData.name = playername + playerData.callsign = playerData.unit:GetCallsign() + playerData.client = CLIENT:FindByName(_unitName, nil, true) + playerData.coalition = _group:GetCoalition() + + playerData.destroy=playerData.destroy or true + playerData.launchalert=playerData.launchalert or true + playerData.marklaunch=playerData.marklaunch or true + + playerData.defeated=playerData.defeated or 0 + playerData.dead=playerData.dead or 0 + + -- Init player data. + self.players[playername]=playerData + + -- Init player grades table if necessary. + --self.playerscores[playername]=self.playerscores[playername] or {} + + end +end + +--- FOX2 event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun). +-- @param #FOX2 self +-- @param Core.Event#EVENTDATA EventData +function FOX2:OnEventShot(EventData) + self:I({eventshot = EventData}) + + if EventData.Weapon==nil then + return + end + if EventData.IniDCSUnit==nil then + return + end + + -- Weapon data. + local _weapon = EventData.WeaponName + local _target = EventData.Weapon:getTarget() + local _targetName = "unknown" + local _targetUnit = nil --Wrapper.Unit#UNIT + + -- Weapon descriptor. + local desc=EventData.Weapon:getDesc() + --self:E({desc=desc}) + + -- Weapon category: 0=Shell, 1=Missile, 2=Rocket, 3=BOMB + local weaponcategory=desc.category + + -- Missile category= + local missilecategory=desc.missileCategory + + -- Debug info. + self:E(FOX2.lid.."EVENT SHOT: FOX2") + self:E(FOX2.lid..string.format("EVENT SHOT: Ini unit = %s", tostring(EventData.IniUnitName))) + self:E(FOX2.lid..string.format("EVENT SHOT: Ini group = %s", tostring(EventData.IniGroupName))) + self:E(FOX2.lid..string.format("EVENT SHOT: Weapon type = %s", tostring(_weapon))) + self:E(FOX2.lid..string.format("EVENT SHOT: Weapon cate = %s", tostring(weaponcategory))) + self:E(FOX2.lid..string.format("EVENT SHOT: Missil cate = %s", tostring(missilecategory))) + if _target then + self:E({target=_target}) + --_targetName=Unit.getName(_target) + --_targetUnit=UNIT:FindByName(_targetName) + _targetUnit=UNIT:Find(_target) + end + self:E(FOX2.lid..string.format("EVENT SHOT: Target name = %s", tostring(_targetName))) + + -- Track missiles of type AAM=1, SAM=2 or OTHER=6 + local _track = weaponcategory==1 and missilecategory and (missilecategory==1 or missilecategory==2 or missilecategory==6) + + -- Get shooter. + local shooterUnit = EventData.IniUnit + local shooterName = EventData.IniUnitName + local shooterCoalition=shooterUnit:GetCoalition() + local shooterCoord=shooterUnit:GetCoordinate() + + -- Only track missiles + if _track then + + local missile={} --#FOX2.MissileData + + --missile.missiletype=_weapontype + + -- Tracking info and init of last bomb position. + self:I(FOX2.lid..string.format("FOX2: Tracking %s - %s.", _weapon, EventData.weapon:getName())) + + -- Loop over players. + for _,_player in pairs(self.players) do + local player=_player --#FOX2.PlayerData + + -- Player position. + local playerUnit=player.unit + + if playerUnit and playerUnit:IsAlive() then + + local distance=playerUnit:GetCoordinate():Get3DDistance(shooterCoord) + local bearing=playerUnit:GetCoordinate():HeadingTo(shooterCoord) + + if _targetUnit and player.launchalert and player.coalition~=shooterCoalition then + + -- Inform players. + local text=string.format("Missile launch detected! Distance %.1f NM, bearing %03d°.", UTILS.MetersToNM(distance), bearing) + MESSAGE:New(text, 5, "ALERT"):ToClient(player.client) + + if player.marklaunch then + local text=string.format("Missile launch coordinates:\n%s\n%s", shooterCoord:ToStringLLDMS(), shooterCoord:ToStringBULLS(player.coalition)) + shooterCoord:MarkToGroup(text, player.group) + end + end + end + end + + -- Init missile position. + local _lastBombPos = {x=0,y=0,z=0} + + -- Target unit of the missile. + local target=nil --Wrapper.Unit#UNIT + + --- Function monitoring the position of a bomb until impact. + local function trackMissile(_ordnance) + + -- When the pcall returns a failure the weapon has hit. + local _status,_bombPos = pcall( + function() + return _ordnance:getPoint() + end) + + --self:T3(FOX2.lid..string.format("FOX2: Missile still in air: %s", tostring(_status))) + if _status then + + ---------------------------------------------- + -- Still in the air. Remember this position -- + ---------------------------------------------- + + -- Missile position. + _lastBombPos = {x=_bombPos.x, y=_bombPos.y, z=_bombPos.z} + + -- Missile coordinate. + local missileCoord=COORDINATE:NewFromVec3(_lastBombPos) + + -- Missile velocity in m/s. + local missileVelocity=UTILS.VecNorm(_ordnance:getVelocity()) + + if _targetUnit then + ----------------------------------- + -- Missile has a specific target -- + ----------------------------------- + + target=_targetUnit + + else + + -- Distance to closest player. + local mindist=nil + + -- Loop over players. + for _,_player in pairs(self.players) do + local player=_player --#FOX2.PlayerData + + -- Player position. + local playerCoord=player.unit:GetCoordinate() + + -- Distance. + local dist=missileCoord:Get3DDistance(playerCoord) + + -- Update mindist if necessary. + if mindist==nil or dist50000 then + -- > 50 km + dt=5.0 + elseif distance>10000 then + -- 10-50 km + dt=1.0 + elseif distance>5000 then + -- 5-10 km + dt=0.5 + elseif distance>1000 then + -- 1-5 km + dt=0.1 + else + -- < 1 km + dt=0.01 + end + + -- Check again in dt seconds. + return timer.getTime()+dt + end + else + + -- No target ==> terminate timer. + return nil + end + + else + + ------------------------------------- + -- Missile does not exist any more -- + ------------------------------------- + + if target then + local player=self:_GetPlayerFromUnitname(target:GetName()) + if player then + local text=string.format("Missile defeated. Well done, %s!", player.name) + MESSAGE:New(text, 10):ToClient(player.client) + end + end + + --Terminate the timer. + self:T(FOX2.lid..string.format("Terminating missile track timer.")) + return nil + + end -- _status check + + end -- end function trackBomb + + -- Weapon is not yet "alife" just yet. Start timer with a little delay. + self:T(FOX2.lid..string.format("Tracking of missile starts in 0.1 seconds.")) + timer.scheduleFunction(trackMissile, EventData.weapon, timer.getTime()+0.1) + + end --if _track + +end + + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RADIO MENU Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add menu commands for player. +-- @param #FOX2 self +-- @param #string _unitName Name of player unit. +function FOX2:_AddF10Commands(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check for player unit. + if _unit and playername then + + -- Get group and ID. + local group=_unit:GetGroup() + local gid=group:GetID() + + if group and gid then + + if not self.menuadded[gid] then + + -- Enable switch so we don't do this twice. + self.menuadded[gid]=true + + -- Set menu root path. + local _rootPath=nil + if FOX2.MenuF10Root then + ------------------------ + -- MISSON LEVEL MENUE -- + ------------------------ + + -- F10/FOX2/... + _rootPath=FOX2.MenuF10Root + + else + ------------------------ + -- GROUP LEVEL MENUES -- + ------------------------ + + -- Main F10 menu: F10/FOX2/ + if FOX2.MenuF10[gid]==nil then + FOX2.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "FOX2") + end + + -- F10/FOX2/... + _rootPath=FOX2.MenuF10[gid] + + end + + + -------------------------------- + -- F10/F FOX2/F1 Help + -------------------------------- + local _helpPath=missionCommands.addSubMenuForGroup(gid, "Help", _rootPath) + -- F10/FOX2/F1 Help/ + --missionCommands.addCommandForGroup(gid, "Subtitles On/Off", _helpPath, self._SubtitlesOnOff, self, _unitName) -- F7 + --missionCommands.addCommandForGroup(gid, "Trapsheet On/Off", _helpPath, self._TrapsheetOnOff, self, _unitName) -- F8 + + ------------------------- + -- F10/F FOX2/ + ------------------------- + + missionCommands.addCommandForGroup(gid, "Launch Alerts On/Off", _rootPath, self._ToggleLaunchAlert, self, _unitName) -- F2 + missionCommands.addCommandForGroup(gid, "Destroy Missiles On/Off", _rootPath, self._ToggleDestroyMissiles, self, _unitName) -- F3 + + end + else + self:E(self.lid..string.format("ERROR: Could not find group or group ID in AddF10Menu() function. Unit name: %s.", _unitName)) + end + else + self:E(self.lid..string.format("ERROR: Player unit does not exist in AddF10Menu() function. Unit name: %s.", _unitName)) + end + +end + + +--- Turn player's launch alert on/off. +-- @param #FOX2 self +-- @param #string _unitname Name of the player unit. +function FOX2:_ToggleLaunchAlert(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#FOX2.PlayerData + + if playerData then + + -- Invert state. + playerData.launchalert=not playerData.launchalert + + -- Inform player. + local text="" + if playerData.launchalert==true then + text=string.format("%s, missile launch alerts are now ENABLED.", playerData.name) + else + text=string.format("%s, missile launch alerts are now DISABLED.", playerData.name) + end + MESSAGE:New(text, 5):ToClient(playerData.client) + + end + end +end + +--- Turn player's +-- @param #FOX2 self +-- @param #string _unitname Name of the player unit. +function FOX2:_ToggleDestroyMissiles(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#FOX2.PlayerData + + if playerData then + + -- Invert state. + playerData.destroy=not playerData.destroy + + -- Inform player. + local text="" + if playerData.destroy==true then + text=string.format("%s, incoming missiles will be DESTROYED.", playerData.name) + else + text=string.format("%s, incoming missiles will NOT be DESTROYED.", playerData.name) + end + MESSAGE:New(text, 5):ToClient(playerData.client) + + end + end +end + + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #FOX2 self +-- @param #string unitName Name of the unit. +-- @return #FOX2.PlayerData Player data. +function FOX2:_GetPlayerFromUnitname(unitName) + + for _,_player in pairs(self.players) do + local player=_player --#FOX2.PlayerData + + if player.unitname==unitName then + return player + end + end + + return nil +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #FOX2 self +-- @param #string _unitName Name of the player unit. +-- @return Wrapper.Unit#UNIT Unit of player or nil. +-- @return #string Name of the player or nil. +function FOX2:_GetPlayerUnitAndName(_unitName) + self:F2(_unitName) + + if _unitName ~= nil then + + -- Get DCS unit from its name. + local DCSunit=Unit.getByName(_unitName) + + if DCSunit then + + -- Get player name if any. + local playername=DCSunit:getPlayerName() + + -- Unit object. + local unit=UNIT:Find(DCSunit) + + -- Debug. + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) + + -- Check if enverything is there. + if DCSunit and unit and playername then + self:T(self.lid..string.format("Found DCS unit %s with player %s.", tostring(_unitName), tostring(playername))) + return unit, playername + end + + end + + end + + -- Return nil if we could not find a player. + return nil,nil +end + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Functional/RAT2.lua b/Moose Development/Moose/Functional/RAT2.lua new file mode 100644 index 000000000..35bb24bb7 --- /dev/null +++ b/Moose Development/Moose/Functional/RAT2.lua @@ -0,0 +1,793 @@ +--- **Ops** - (R2.5) - Random Air Traffic. +-- +-- +-- +-- RAT2 creates random air traffic on the map. +-- +-- +-- +-- **Main Features:** +-- +-- * It's very random. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Functional.Rat2 +-- @image Functional_Rat2.png + + +--- RAT2 class. +-- @type RAT2 +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @extends Core.Fsm#FSM + +--- Be surprised! +-- +-- === +-- +-- ![Banner Image](..\Presentations\RAT2\RAT2_Main.png) +-- +-- # The RAT2 Concept +-- +-- +-- +-- @field #RAT2 +RAT2 = { + ClassName = "RAT2", + Debug = false, + lid = nil, +} + +--- Aircraft types capable of landing on carrier (human+AI). +-- @type RAT2.Ratcraft +-- @field Functional.RatCraft#RATCRAFT ratcraft RAT aircraft group. +-- @field #number ntot Total number of alive aircraft groups alive. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new RAT2 class object. +-- @param #RAT2 self +-- @return #RAT2 self. +function RAT2:New() + + -- Inherit everthing from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #RAT2 + + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Load", "Stopped") -- Load player scores from file. + self:AddTransition("Stopped", "Start", "Running") -- Start RAT2 script. + self:AddTransition("*", "Status", "*") -- Start RAT2 script. + +end + +--- Create a new RAT2 class object. +-- @param #RAT2 self +-- @return #RAT2 self. +function RAT2:AddAircraft(template, n) + + +end + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status Functions +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check spawn queue and spawn aircraft if necessary. +-- @param #RAT2 self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RAT2:onafterStatus(From, Event, To) + +end + + +--- Check spawn queue and spawn aircraft if necessary. +-- @param #RAT2 self +function RAT2:_CheckQueue() + + for i,_ratcraft in pairs(self.Qspawn) do + local ratcraft=_ratcraft --Functional.RatCraft#RATCRAFT + + --- Check if + -- Time has passed. + -- Already enough aircraft are alive + + local nalive=ratcraft:_GetAliveGroups() + + if naliveDeltaholdingMax then + h_holding=math.abs(DeltaholdingMax) + end + + -- This is the height ASL of the holding point we want to fly to. + local Hh_holding=H_holding+h_holding + + --------------------------- + --- Max Flight Altitude --- + --------------------------- + + -- Get max flight altitude relative to H_departure. + local h_max=self:_GetMaxHeight(d_total, AlphaClimb, AlphaDescent, H_departure, H_holding, h_holding) + + -- Max flight level ASL aircraft can reach for given angles and distance. + local FLmax = h_max+H_departure + + --CRUISE + -- Min cruise alt is just above holding point at destination or departure height, whatever is larger. + local FLmin=math.max(H_departure, Hh_holding) + + -- Ensure that FLmax not above its service ceiling. + FLmax=math.min(FLmax, ceiling) + + -- If the route is very short we set FLmin a bit lower than FLmax. + if FLmin>FLmax then + FLmin=FLmax + end + + -- Expected cruise altitude - peak of gaussian distribution. + if FLcruise_expectFLmax then + FLcruise_expect=FLmax + end + + -- Set cruise altitude. Selected from Gaussian distribution but limited to FLmin and FLmax. + local FLcruise=UTILS.RandomGaussian(FLcruise_expect, math.abs(FLmax-FLmin)/4, FLmin, FLmax) + + -- Climb and descent heights. + local h_climb = FLcruise - H_departure + local h_descent = FLcruise - Hh_holding + + -- Get distances. + local d_climb = h_climb/math.tan(AlphaClimb) + local d_descent = h_descent/math.tan(AlphaDescent) + local d_cruise = d_total-d_climb-d_descent + + -- Debug. + local text=string.format("Flight plan:\n") + text=text..string.format("Vx max = %.2f km/h\n", Vmax*3.6) + text=text..string.format("Vx climb = %.2f km/h\n", VxClimb*3.6) + text=text..string.format("Vx cruise = %.2f km/h\n", VxCruise*3.6) + text=text..string.format("Vx descent = %.2f km/h\n", VxDescent*3.6) + text=text..string.format("Vx holding = %.2f km/h\n", VxHolding*3.6) + text=text..string.format("Vx final = %.2f km/h\n", VxFinal*3.6) + text=text..string.format("Vy max = %.2f m/s\n", Vymax) + text=text..string.format("Vy climb = %.2f m/s\n", VyClimb) + text=text..string.format("Alpha Climb = %.2f Deg\n", math.deg(AlphaClimb)) + text=text..string.format("Alpha Descent = %.2f Deg\n", math.deg(AlphaDescent)) + text=text..string.format("Dist climb = %.3f km\n", d_climb/1000) + text=text..string.format("Dist cruise = %.3f km\n", d_cruise/1000) + text=text..string.format("Dist descent = %.3f km\n", d_descent/1000) + text=text..string.format("Dist total = %.3f km\n", d_total/1000) + text=text..string.format("h_climb = %.3f km\n", h_climb/1000) + text=text..string.format("h_desc = %.3f km\n", h_descent/1000) + text=text..string.format("h_holding = %.3f km\n", h_holding/1000) + text=text..string.format("h_max = %.3f km\n", h_max/1000) + text=text..string.format("FL min = %.3f km\n", FLmin/1000) + text=text..string.format("FL expect = %.3f km\n", FLcruise_expect/1000) + text=text..string.format("FL cruise * = %.3f km\n", FLcruise/1000) + text=text..string.format("FL max = %.3f km\n", FLmax/1000) + text=text..string.format("Ceiling = %.3f km\n", ceiling/1000) + text=text..string.format("Max range = %.3f km\n", Range/1000) + self:T(self.wid..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 + + ------------------------ + --- Create Waypoints --- + ------------------------ + + -- Waypoints and coordinates + local wp={} + local c={} + + --- Departure/Take-off + c[#c+1]=Pdeparture + wp[#wp+1]=Pdeparture:WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, VxClimb, true, departure, nil, "Departure") + + --- Begin of Cruise + local Pcruise=Pdeparture:Translate(d_climb, heading) + Pcruise.y=FLcruise + c[#c+1]=Pcruise + wp[#wp+1]=Pcruise:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxCruise, true, nil, nil, "Cruise") + + --- Descent + local Pdescent=Pcruise:Translate(d_cruise, heading) + Pdescent.y=FLcruise + c[#c+1]=Pdescent + wp[#wp+1]=Pdescent:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxDescent, true, nil, nil, "Descent") + + --- Holding point + Pholding.y=H_holding+h_holding + c[#c+1]=Pholding + wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding, true, nil, nil, "Holding") + + --- Final destination. + c[#c+1]=Pdestination + wp[#wp+1]=Pdestination:WaypointAir("RADIO", COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, VxFinal, true, destination, nil, "Final Destination") + + + -- Mark points at waypoints for debugging. + if self.Debug then + for i,coord in pairs(c) do + local coord=coord --Core.Point#COORDINATE + local dist=0 + if i>1 then + dist=coord:Get2DDistance(c[i-1]) + end + coord:MarkToAll(string.format("Waypoint %i, distance = %.2f km",i, dist/1000)) + end + end + + return wp,c +end + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Spawn functions +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Spawn an aircraft asset (plane or helo) at the airbase associated with the warehouse. +-- @param #RAT2 self +-- @param Funtional.RatCraft#RATCRAFT ratcraft +-- @return #boolean If true, ratcraft was spawned. +function RAT2:_TrySpawnRatcraft(ratcraft) + + -- TODO: loop over departure airbases. + + for _,airbase in pairs(ratcraft.departures) do + + -- Get parking data. + local parking=self:_FindParking(airbase, ratcraft) + + -- Check if enough parking is available. + if parking then + self:_SpawnRatcraft() + return true + end + + end + + + return false +end + +--- Spawn an aircraft asset (plane or helo) at the airbase associated with the warehouse. +-- @param #RAT2 self +-- @param #string alias Alias name of the asset group. +-- @param #WAREHOUSE.Assetitem asset Ground asset that will be spawned. +-- @param #WAREHOUSE.Queueitem request Request belonging to this asset. Needed for the name/alias. +-- @param #table parking Parking data for this asset. +-- @param #boolean uncontrolled Spawn aircraft in uncontrolled state. +-- @return Wrapper.Group#GROUP The spawned group or nil if the group could not be spawned. +function RAT2:_SpawnRatcraft(ratcraft, departure, destination, parking, uncontrolled) + + -- Prepare the spawn template. + local template=self:_SpawnAssetPrepareTemplate(ratcraft, alias) + + -- Get flight path if the group goes to another warehouse by itself. + template.route.points=self:_GetFlightplan(asset, self.airbase, request.warehouse.airbase) + + -- Get airbase ID and category. + local AirbaseID = self.airbase:GetID() + local AirbaseCategory = self:GetAirbaseCategory() + + -- Check enough parking spots. + if AirbaseCategory==Airbase.Category.HELIPAD or AirbaseCategory==Airbase.Category.SHIP then + + --TODO Figure out what's necessary in this case. + + else + + if #parking<#template.units then + local text=string.format("ERROR: Not enough parking! Free parking = %d < %d aircraft to be spawned.", #parking, #template.units) + self:_DebugMessage(text) + return nil + end + + end + + -- Position the units. + for i=1,#template.units do + + -- Unit template. + local unit = template.units[i] + + if AirbaseCategory == Airbase.Category.HELIPAD or AirbaseCategory == Airbase.Category.SHIP then + + -- Helipads we take the position of the airbase location, since the exact location of the spawn point does not make sense. + local coord=self.airbase:GetCoordinate() + + unit.x=coord.x + unit.y=coord.z + unit.alt=coord.y + + unit.parking_id = nil + unit.parking = nil + + else + + local coord=parking[i].Coordinate --Core.Point#COORDINATE + local terminal=parking[i].TerminalID --#number + + if self.Debug then + coord:MarkToAll(string.format("Spawnplace unit %s terminal %d.", unit.name, terminal)) + end + + unit.x=coord.x + unit.y=coord.z + unit.alt=coord.y + + unit.parking_id = nil + unit.parking = terminal + + end + + if asset.livery then + unit.livery_id = asset.livery + end + if asset.skill then + unit.skill= asset.skill + end + + end + + -- And template position. + template.x = template.units[1].x + template.y = template.units[1].y + + -- Uncontrolled spawning. + template.uncontrolled=uncontrolled + + -- Debug info. + self:T2({airtemplate=template}) + + -- Spawn group. + local group=_DATABASE:Spawn(template) --Wrapper.Group#GROUP + + return group +end + + +--- Prepare a spawn template for the asset. Deep copy of asset template, adjusting template and unit names, nillifying group and unit ids. +-- @param #RAT2 self +-- @param Functional.RatCraft#RATCRAFT ratcraft Aircraft that will be spawned. +-- @param #string alias Alias name of the group. +-- @return #table Prepared new spawn template. +function RAT2:_SpawnAssetPrepareTemplate(ratcraft, alias) + + -- Create an own copy of the template! + local template=UTILS.DeepCopy(ratcraft.template) + + -- Set unique name. + template.name=alias + + -- Set current(!) coalition and country. + template.CoalitionID=ratcraft.coalition + template.CountryID=ratcraft.country + + -- Nillify the group ID. + template.groupId=nil + + -- No late activation. + template.lateActivation=false + + -- Set and empty route. + template.route = {} + template.route.routeRelativeTOT=true + template.route.points = {} + + -- Handle units. + for i=1,#template.units do + + -- Unit template. + local unit = template.units[i] + + -- Nillify the unit ID. + unit.unitId=nil + + -- Set unit name: -01, -02, ... + unit.name=string.format("%s-%02d", template.name , i) + + end + + return template +end + +--- Get the proper terminal type based on generalized attribute of the group. +--@param #RAT2 self +--@param #RATCRAFT.Attribute _attribute Generlized attribute of unit. +--@param #number _category Airbase category. +--@return Wrapper.Airbase#AIRBASE.TerminalType Terminal type for this group. +function RAT2:_GetTerminal(_attribute, _category) + + -- Default terminal is "large". + local _terminal=AIRBASE.TerminalType.OpenBig + + if _attribute==RATCRAFT.Attribute.FIGHTER then + -- Fighter ==> small. + _terminal=AIRBASE.TerminalType.FighterAircraft + elseif _attribute==RATCRAFT.Attribute.BOMBER or _attribute==RATCRAFT.Attribute.TRANSPORTPLANE or _attribute==RATCRAFT.Attribute.TANKER or _attribute==RATCRAFT.Attribute.AWACS then + -- Bigger aircraft. + _terminal=AIRBASE.TerminalType.OpenBig + elseif _attribute==RATCRAFT.Attribute.TRANSPORTHELO or _attribute==RATCRAFT.Attribute.ATTACKHELO then + -- Helicopter. + _terminal=AIRBASE.TerminalType.HelicopterUsable + else + --_terminal=AIRBASE.TerminalType.OpenMedOrBig + end + + -- For ships, we allow medium spots for all fixed wing aircraft. There are smaller tankers and AWACS aircraft that can use a carrier. + if _category==Airbase.Category.SHIP then + if not (_attribute==RATCRAFT.Attribute.TRANSPORTHELO or _attribute==RATCRAFT.Attribute.ATTACKHELO) then + _terminal=AIRBASE.TerminalType.OpenMedOrBig + end + end + + return _terminal +end + + +--- Seach unoccupied parking spots at the airbase for a list of assets. For each asset group a list of parking spots is returned. +-- During the search also the not yet spawned asset aircraft are considered. +-- If not enough spots for all asset units could be found, the routine returns nil! +-- @param #RAT2 self +-- @param Wrapper.Airbase#AIRBASE airbase The airbase where we search for parking spots. +-- @param #table assets A table of assets for which the parking spots are needed. +-- @return #table Table of coordinates and terminal IDs of free parking spots. Each table entry has the elements .Coordinate and .TerminalID. +function RAT2:_FindParking(airbase, ratcraft) + + -- Init default + local scanradius=100 + local scanunits=true + local scanstatics=true + local scanscenery=false + local verysafe=false + + -- Function calculating the overlap of two (square) objects. + local function _overlap(l1,l2,dist) + local safedist=(l1/2+l2/2)*1.05 -- 5% safety margine added to safe distance! + local safe = (dist > safedist) + self:T3(string.format("l1=%.1f l2=%.1f s=%.1f d=%.1f ==> safe=%s", l1,l2,safedist,dist,tostring(safe))) + return safe + end + + -- Get parking spot data table. This contains all free and "non-free" spots. + local parkingdata=airbase:GetParkingSpotsTable() + + -- List of obstacles. + local obstacles={} + + -- Loop over all parking spots and get the currently present obstacles. + -- How long does this take on very large airbases, i.e. those with hundereds of parking spots? Seems to be okay! + for _,parkingspot in pairs(parkingdata) do + + -- Coordinate of the parking spot. + local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE + local _termid=parkingspot.TerminalID + + -- Scan a radius of 100 meters around the spot. + local _,_,_,_units,_statics,_sceneries=_spot:ScanObjects(scanradius, scanunits, scanstatics, scanscenery) + + -- Check all units. + for _,_unit in pairs(_units) do + local unit=_unit --Wrapper.Unit#UNIT + local _coord=unit:GetCoordinate() + local _size=self:_GetObjectSize(unit:GetDCSObject()) + local _name=unit:GetName() + table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="unit"}) + end + + -- Check all statics. + for _,static in pairs(_statics) do + local _vec3=static:getPoint() + local _coord=COORDINATE:NewFromVec3(_vec3) + local _name=static:getName() + local _size=self:_GetObjectSize(static) + table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="static"}) + end + + -- Check all scenery. + for _,scenery in pairs(_sceneries) do + local _vec3=scenery:getPoint() + local _coord=COORDINATE:NewFromVec3(_vec3) + local _name=scenery:getTypeName() + local _size=self:_GetObjectSize(scenery) + table.insert(obstacles,{coord=_coord, size=_size, name=_name, type="scenery"}) + end + + -- TODO check clients. Clients cannot be spawned. So we can loop over them. + + end + + -- Parking data for all assets. + local parking={} + + -- Get terminal type of this asset + local terminaltype=self:_GetTerminal(ratcraft.attribute, self:GetAirbaseCategory()) + + -- Loop over all units - each one needs a spot. + --TODO: nunits should be counted from alive units. + for i=1,ratcraft.nunits do + + -- Loop over all parking spots. + local gotit=false + for _,_parkingspot in pairs(parkingdata) do + local parkingspot=_parkingspot --Wrapper.Airbase#AIRBASE.ParkingSpot + + -- Check correct terminal type for asset. We don't want helos in shelters etc. + if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) then + + -- Coordinate of the parking spot. + local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE + local _termid=parkingspot.TerminalID + local _toac=parkingspot.TOAC + + --env.info(string.format("FF asset=%s (id=%d): needs terminal type=%d, id=%d, #obstacles=%d", _asset.templatename, _asset.uid, terminaltype, _termid, #obstacles)) + + local free=true + local problem=nil + + -- Safe parking using TO_AC from DCS result. + if self.safeparking and _toac then + free=false + self:T("Parking spot %d is occupied by other aircraft taking off or landing.", _termid) + end + + -- Loop over all obstacles. + for _,obstacle in pairs(obstacles) do + + -- Check if aircraft overlaps with any obstacle. + local dist=_spot:Get2DDistance(obstacle.coord) + -- TODO: ratcraft size! + local safe=_overlap(ratcraft.size, obstacle.size, dist) + + -- Spot is blocked. + if not safe then + --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is NOT SAFE", _asset.templatename, _asset.uid, _termid, dist)) + free=false + problem=obstacle + problem.dist=dist + break + else + --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is SAFE", _asset.templatename, _asset.uid, _termid, dist)) + end + + end + + -- Check if spot is free + if free then + + -- Add parkingspot for this asset unit. + table.insert(parking, parkingspot) + + self:T(self.wid..string.format("Parking spot #%d is free for ratcraft unit id=%d!", _termid, i)) + + -- Add the unit as obstacle so that this spot will not be available for the next unit. + -- TODO: ratcraft templatename. + table.insert(obstacles, {coord=_spot, size=ratcraft.size, name=ratcraft.templatename, type="asset"}) + + -- Break loop over parking spots. + gotit=true + break + + else + + -- Debug output for occupied spots. + self:T(self.wid..string.format("Parking spot #%d is occupied or not big enough!", _termid)) + if self.Debug then + local coord=problem.coord --Core.Point#COORDINATE + local text=string.format("Obstacle blocking spot #%d is %s type %s with size=%.1f m and distance=%.1f m.", _termid, problem.name, problem.type, problem.size, problem.dist) + coord:MarkToAll(string.format(text)) + end + + end + + end -- check terminal type + end -- loop over parking spots + + -- No parking spot for at least one unit :( + if not gotit then + self:T(self.wid..string.format("WARNING: No free parking spot for ratcraft unit i=%d", i)) + return nil + end + + end -- loop over units + + return parking +end + + + + + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Flightplan functions +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index c3f9ee12e..62e39cadf 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -290,7 +290,7 @@ RANGE.id="RANGE | " --- Range script version. -- @field #string version -RANGE.version="1.2.5" +RANGE.version="1.3.0" --TODO list: --TODO: Verbosity level for messages. @@ -1183,25 +1183,39 @@ end function RANGE:OnEventShot(EventData) self:F({eventshot = EventData}) + -- Nil checks. + if EventData.Weapon==nil then + return + end + if EventData.IniDCSUnit==nil then + return + end + -- Weapon data. local _weapon = EventData.Weapon:getTypeName() -- should be the same as Event.WeaponTypeName local _weaponStrArray = UTILS.Split(_weapon,"%.") local _weaponName = _weaponStrArray[#_weaponStrArray] + -- Weapon descriptor. + local desc=EventData.Weapon:getDesc() + + -- Weapon category: 0=SHELL, 1=MISSILE, 2=ROCKET, 3=BOMB (Weapon.Category.X) + local weaponcategory=desc.category + -- Debug info. self:T(RANGE.id.."EVENT SHOT: Range "..self.rangename) self:T(RANGE.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) self:T(RANGE.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) self:T(RANGE.id.."EVENT SHOT: Weapon type = ".._weapon) self:T(RANGE.id.."EVENT SHOT: Weapon name = ".._weaponName) - + self:T(RANGE.id.."EVENT SHOT: Weapon cate = "..weaponcategory) -- Special cases: - local _viggen=string.match(_weapon, "ROBOT") or string.match(_weapon, "RB75") or string.match(_weapon, "BK90") or string.match(_weapon, "RB15") or string.match(_weapon, "RB04") + --local _viggen=string.match(_weapon, "ROBOT") or string.match(_weapon, "RB75") or string.match(_weapon, "BK90") or string.match(_weapon, "RB15") or string.match(_weapon, "RB04") -- Tracking conditions for bombs, rockets and missiles. - local _bombs=string.match(_weapon, "weapons.bombs") - local _rockets=string.match(_weapon, "weapons.nurs") - local _missiles=string.match(_weapon, "weapons.missiles") or _viggen + local _bombs = weaponcategory==Weapon.Category.BOMB --string.match(_weapon, "weapons.bombs") + local _rockets = weaponcategory==Weapon.Category.ROCKET --string.match(_weapon, "weapons.nurs") + local _missiles = weaponcategory==Weapon.Category.MISSILE --string.match(_weapon, "weapons.missiles") or _viggen -- Check if any condition applies here. local _track = (_bombs and self.trackbombs) or (_rockets and self.trackrockets) or (_missiles and self.trackmissiles) @@ -1221,8 +1235,8 @@ function RANGE:OnEventShot(EventData) self:T(RANGE.id..string.format("Range %s, player %s, player-range distance = %d km.", self.rangename, _playername, dPR/1000)) end - -- Only track if distance player to range is < 25 km. - if _track and dPR<=self.BombtrackThreshold then + -- Only track if distance player to range is < 25 km. Also check that a player shot. No need to track AI weapons. + if _track and dPR<=self.BombtrackThreshold and _unit and _playername then -- Tracking info and init of last bomb position. self:T(RANGE.id..string.format("RANGE %s: Tracking %s - %s.", self.rangename, _weapon, EventData.weapon:getName())) @@ -1346,8 +1360,8 @@ function RANGE:OnEventShot(EventData) end -- end function trackBomb -- Weapon is not yet "alife" just yet. Start timer in one second. - self:T(RANGE.id..string.format("Range %s, player %s: Tracking of weapon starts in one second.", self.rangename, _playername)) - timer.scheduleFunction(trackBomb, EventData.weapon, timer.getTime() + 1.0) + self:T(RANGE.id..string.format("Range %s, player %s: Tracking of weapon starts in 0.1 seconds.", self.rangename, _playername)) + timer.scheduleFunction(trackBomb, EventData.weapon, timer.getTime()+0.1) end --if _track (string.match) and player-range distance < threshold. diff --git a/Moose Development/Moose/Functional/RatCraft.lua b/Moose Development/Moose/Functional/RatCraft.lua new file mode 100644 index 000000000..b840ead15 --- /dev/null +++ b/Moose Development/Moose/Functional/RatCraft.lua @@ -0,0 +1,166 @@ +--- **Ops** - (R2.5) - Random Air Traffic. +-- +-- +-- +-- RAT2 creates random air traffic on the map. +-- +-- +-- +-- **Main Features:** +-- +-- * It's very random. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Functional.Ratcraft +-- @image Functional_Ratcraft.png + + +--- RATCRAFT class. +-- @type RATCRAFT +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @field #number coalition Coalition side. +-- @field #number country Country number. +-- @field #number size Max size in meters. +-- @field #table liveries Table of liveries. +-- @field #string livery Livery. +-- @field #string actype Aircraft type name. +-- @field #number category Category (plane=X, helo=Y). +-- @field #RATCRAFT.Attribute attribute Attribute. +-- @field #table template Spawn template table. +-- @extends Core.Fsm#FSM + +--- Be surprised! +-- +-- === +-- +-- ![Banner Image](..\Presentations\RAT2\RAT2_Main.png) +-- +-- # The RAT2 Concept +-- +-- +-- +-- @field #RATCRAFT +RATCRAFT = { + ClassName = "RATCRAFT", + Debug = false, + lid = nil, + liveries = {}, + livery = nil, + actype = nil, + attribute = nil, + ceiling = nil, + speedmax = nil, + sizex = nil, + sizez = nil, + size = nil, + commute = nil, + coalition = nil, + country = nil, + skill = nil, + onboard = nil, + departures = {}, + destinations = {}, + departure = nil, + destination = nil, + commute = nil, + takeoff = nil, + landing = nil, +} + +--- Generalized asset attributes. Can be used to request assets with certain general characteristics. See [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes) on hoggit. +-- @type RATCRAFT.Attribute +-- @field #string TRANSPORTPLANE Airplane with transport capability. This can be used to transport other assets. +-- @field #string AWACS Airborne Early Warning and Control System. +-- @field #string FIGHTER Fighter, interceptor, ... airplane. +-- @field #string BOMBER Aircraft which can be used for strategic bombing. +-- @field #string TANKER Airplane which can refuel other aircraft. +-- @field #string TRANSPORTHELO Helicopter with transport capability. This can be used to transport other assets. +-- @field #string ATTACKHELO Attack helicopter. +-- @field #string UAV Unpiloted Aerial Vehicle, e.g. drones. +RATCRAFT.Attribute = { + TRANSPORTPLANE="TransportPlane", + AWACS="AWACS", + FIGHTER="Fighter", + BOMBER="Bomber", + TANKER="Tanker", + TRANSPORTHELO="TransportHelo", + ATTACKHELO="AttackHelo", + UAV="UAV", + OTHER="Other", +} + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new AIRBOSS class object for a specific aircraft carrier unit. +-- @param #RATCRAFT self +-- @return #RATCRAFT self +function RATCRAFT:New(group) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #RATCRAFT + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Load", "Stopped") -- Load player scores from file. + self:AddTransition("Stopped", "Start", "Running") -- Start RAT2 script. + +end + +function RATCRAFT:_Init() + + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add a departure to +-- @param #RATCRAFT self +-- @return #RATCRAFT self +function RATCRAFT:AddDeparture(departure) + + if type(departure)=="string" then + + else + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Departure Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get departure. +-- @param #RATCRAFT self +-- @return Wrapper.Airbase#AIRBASE +function RATCRAFT:_GetDeparture() + + + + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get number of alive groups +-- @param #RATCRAFT self +-- @return #number N live. +function RATCRAFT:_GetAliveGroups() + + return 0 +end + + diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 68e0125d9..0cd58141f 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -60,10 +60,15 @@ __Moose.Include( 'Scripts/Moose/Functional/Artillery.lua' ) __Moose.Include( 'Scripts/Moose/Functional/Suppression.lua' ) __Moose.Include( 'Scripts/Moose/Functional/PseudoATC.lua' ) __Moose.Include( 'Scripts/Moose/Functional/Warehouse.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/RAT2.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/RatCraft.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Fox2.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/FlightModelData.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Airboss.lua' ) __Moose.Include( 'Scripts/Moose/Ops/RecoveryTanker.lua' ) __Moose.Include( 'Scripts/Moose/Ops/RescueHelo.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/FlightControl.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Balancer.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Air.lua' ) diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua new file mode 100644 index 000000000..72fda8f32 --- /dev/null +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -0,0 +1,1518 @@ +--- **ATC** - (R2.5) - Manage recovery of aircraft at airdromes and FARPS. +-- +-- +-- +-- **Main Features:** +-- +-- * Manage aircraft recovery. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module ATC.FlightControl +-- @image ATC_FlightControl.png + + +--- FLIGHTCONTROL class. +-- @type FLIGHTCONTROL +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string theatre The DCS map used in the mission. +-- @field #string lid Class id string for output to DCS log file. +-- @field #string airbasename Name of airbase. +-- @field #number airbasetype Type of airbase. +-- @field Wrapper.Airbase#AIRBASE airbase Airbase object. +-- @field Core.Zone#ZONE zoneAirbase Zone around the airbase. +-- @field #table parking Parking spots table. +-- @field #table runways Runway table. +-- @field #table flight All flights table. +-- @field #table Qwaiting Queue of aircraft waiting for landing permission. +-- @field #table Qlanding Queue of aircraft currently on final approach. +-- @field #table Qtakeoff Queue of aircraft about to takeoff. +-- @field #table Qparking Queue of aircraft parking. +-- @field #number activerwyno Number of active runway. +-- @extends Core.Fsm#FSM + +--- Be surprised! +-- +-- === +-- +-- ![Banner Image](..\Presentations\FLIGHTCONTROL\FlightControl_Main.jpg) +-- +-- # The FLIGHTCONTROL Concept +-- +-- +-- +-- @field #FLIGHTCONTROL +FLIGHTCONTROL = { + ClassName = "FLIGHTCONTROL", + Debug = false, + lid = nil, + theatre = nil, + airbasename = nil, + airbase = nil, + airbasetype = nil, + zoneAirbase = nil, + parking = {}, + runways = {}, + flights = {}, + Qwaiting = {}, + Qlanding = {}, + Qtakeoff = {}, + Qparking = {}, + activerwyno = 1, +} + +--- Parameters of a flight group. +-- @type FLIGHTCONTROL.FlightGroup +-- @field #string groupname Name of the group. +-- @field Wrapper.Group#GROUP group Flight group. +-- @field #number nunits Number of units in group. +-- @field #number time Timestamp in seconds of timer.getAbsTime() of the last important event, e.g. added to the queue. +-- @field #number dist0 Distance in meters when group was first detected. +-- @field #number flag Flag value describing the current stack. +-- @field #boolean ai If true, flight is purly AI. +-- @field #string actype Aircraft type name. +-- @field #table onboardnumbers Onboard numbers of aircraft in the group. +-- @field #string onboard Onboard number of player or first unit in group. +-- @field #boolean holding If true, flight is in holding zone. +-- @field #boolean inzone If true, flight is inside airbase zone. +-- @field #table elements Flight group elements. + +--- Parameters of an element in a flight group. +-- @type FLIGHTCONTROL.FlightElement +-- @field #string unitname Name of the unit. +-- @field Wrapper.Unit#UNIT unit Aircraft unit. +-- @field #boolean ai If true, AI sits inside. If false, human player is flying. +-- @field #string onboard Onboard number of the aircraft. +-- @field #boolean recovered If true, element was successfully recovered. +-- @field #boolean tookoff If true, element took off. +-- @field #boolean parking If true, element is parking. +-- @field #number sizemax Max size (length or width) of aircraft in meters. + +--- Parameters of an element in a flight group. +-- @type FLIGHTCONTROL.FlightState +-- @field #string LANDED +-- @field #string PARKED +-- @field #string HOLDING +-- @field #string TAXIING +FLIGHTCONTROL.FlightState={ + LANDED="Landed", + PARKED="Parked", + HOLDING="Holding", + TAXIING="Taxiing", +} + +--- Holding point +-- @type FLIGHTCONTROL.HoldingPoint +-- @field Core.Point#COORDINATE pos0 First poosition of racetrack holding point. +-- @field Core.Point#COORDINATE pos1 Second position of racetrack holding point. +-- @field #number angelsmin Smallest holding altitude in angels. +-- @field #number angelsmax Largest holding alitude in angels. + + +--- Parking spot data. +-- @type FLIGHTCONTROL.ParkingSpot +-- @field #number index Parking index. +-- @field #number id Parking id. +-- @field Core.Point#COORDINATE position Coordinate of the spot. +-- @field #number terminal Terminal type. +-- @field #boolean free If true, spot is free. +-- @field #number drunway Distance to runway. +-- @field #boolean reserved If true, reserved. +-- @field #number markerid ID of the marker. + +--- Runway data. +-- @type FLIGHTCONTROL.Runway +-- @field #number direction Direction of the runway. +-- @field #number length Length of runway in meters. +-- @field #number width Width of runway in meters. +-- @field Core.Point#COORDINATE position Position of runway start. + +--- FlightControl class version. +-- @field #string version +FLIGHTCONTROL.version="0.0.2" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add FARPS. +-- TODO: Add helos. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new FLIGHTCONTROL class object for a specific aircraft carrier unit. +-- @param #FLIGHTCONTROL self +-- @param #string airbasename Name of the airbase. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:New(airbasename) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #FLIGHTCONTROL + + self.airbase=AIRBASE:FindByName(airbasename) + + -- Name of the airbase. + self.airbasename=airbasename + + -- Airbase category airdrome, FARP, SHIP. + self.airbasetype=self.airbase:GetDesc().category + + -- Set some string id for output to DCS.log file. + self.lid=string.format("FLIGHTCONTROL %s |", airbasename) + + -- Current map. + self.theatre=env.mission.theatre + + -- 30 NM zone around the airbase. + self.zoneAirbase=ZONE_RADIUS:New("FC", self:GetCoordinate():GetVec2(), UTILS.NMToMeters(30)) + + -- Init runways. + self:_InitRunwayData() + + -- Init parking spots. + self:_InitParkingSpots() + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- Update status. + + -- Debug trace. + if true then + self.Debug=true + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(3) + --self.dTstatus=0.1 + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Fuctions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set runway. This clears all auto generated runways. +-- @param #FLIGHTCONTROL self +-- @param #FLIGHTCONTROL.Runway Runway. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetRunway(runway) + + -- Reset table. + self.runways={} + + -- Set runway. + table.insert(self.runways, runway) + + return self +end + +--- Add runway. +-- @param #FLIGHTCONTROL self +-- @param #FLIGHTCONTROL.Runway Runway. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:AddRunway(runway) + + -- Set runway. + table.insert(self.runways, runway) + + return self +end + +--- Set active runway number. Counting refers to the position in the table entry. +-- @param #FLIGHTCONTROL self +-- @param #number no Number in the runways table. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetActiveRunwayNumber(no) + self.activerwyno=no + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start FLIGHTCONTROL FSM. Handle events. +-- @param #FLIGHTCONTROL self +function FLIGHTCONTROL:onafterStart() + + -- Events are handled my MOOSE. + self:I(self.lid..string.format("Starting FLIGHTCONTROL v%s for airbase %s of type %d on map %s.", FLIGHTCONTROL.version, self.airbasename, self.airbasetype, self.theatre)) + + -- Handle events. + self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.EngineStartup) + self:HandleEvent(EVENTS.Takeoff) + self:HandleEvent(EVENTS.Land) + self:HandleEvent(EVENTS.EngineShutdown) + self:HandleEvent(EVENTS.Crash) + + -- Init status updates. + self:__Status(-1) +end + +--- Update status. +-- @param #FLIGHTCONTROL self +function FLIGHTCONTROL:onafterStatus() + + -- Check zone for flights inbound. + self:_CheckInbound() + + --self:_UpdateParkingSpots() + + -- Check parking spots. + self:_CheckParking() + + -- Check waiting and landing queue. + self:_CheckQueues() + + -- Get runway. + local runway=self:_GetActiveRunway() + + -- Get free parking spots. + local nfree=self:_GetFreeParkingSpots() + + -- Info text. + local text=string.format("State %s - Active Runway %03d - Free Parking %d", self:GetState(), runway.direction, nfree) + self:I(self.lid..text) + + self:__Status(-30) +end + +--- Initialize data of runways. +-- @param #FLIGHTCONTROL self +function FLIGHTCONTROL:_InitRunwayData() + + -- Get spawn points on runway. + local runwaycoords=self.airbase:GetParkingSpotsCoordinates(AIRBASE.TerminalType.Runway) + + self:E(self.lid..string.format("Runway coords # = %d", #runwaycoords)) + + for i=1,#runwaycoords,2 do + + -- Assuming each runway has two points. + local j=(i+1)/2 + + -- Coordinates of the two runway points. + local c1=runwaycoords[i] --Core.Point#COORDINATES + local c2=runwaycoords[i+1] --Core.Point#COORDINATES + + -- Debug mark + c1:MarkToAll("Runway Point 1") + c2:MarkToAll("Runway Point 2") + + -- Heading of runway. + local hdg=c1:HeadingTo(c2) + + -- Debug info. + self:T(self.lid..string.format("Runway %d heading=%03d°", j, hdg)) + + -- Runway table. + local runway={} --#FLIGHTCONTROL.Runway + runway.direction=hdg + runway.length=c1:Get2DDistance(c2) + runway.position=c1 + + -- Add runway. + table.insert(self.runways, runway) + + -- Inverse runway. + local runway={} --#FLIGHTCONTROL.Runway + local hdg=hdg-180 + if hdg<0 then + hdg=hdg+360 + end + runway.direction=hdg + runway.length=c1:Get2DDistance(c2) + runway.position=c2 + + -- Add inverse runway. + table.insert(self.runways, runway) + end + +end + +--- Get the active runway based on current wind direction. +-- @param #FLIGHTCONTROL self +-- @return #FLIGHTCONTROL.Runway Active runway. +function FLIGHTCONTROL:_GetActiveRunway() + -- TODO: get runway. + local i=math.max(self.activerwyno, #self.runways) + return self.runways[i] +end + +--- Init parking spots. +-- @param #FLIGHTCONTROL self +function FLIGHTCONTROL:_InitParkingSpots() + + -- Parking spots of airbase. + local parkingdata=self.airbase:GetParkingSpotsTable() + + self.parking={} + + for _,_spot in pairs(parkingdata) do + local spot=_spot --Wrapper.Airbase#AIRBASE.ParkingData + + local parking={} --#FLIGHTCONTROL.ParkingSpot + + parking.position=spot.Coordinate + parking.drunway=spot.DistToRwy + parking.terminal=spot.TerminalType + parking.id=spot.TerminalID + parking.free=spot.Free + parking.reserved=spot.TOAC + + -- Mark position. + local text=string.format("ID=%d, Terminal=%d, Free=%s, Reserved=%s, Dist=%.1f", parking.id, parking.terminal, tostring(parking.free), tostring(parking.reserved), parking.drunway) + parking.markerid=parking.position:MarkToAll(text) + + -- Add to table. + table.insert(self.parking, parking) + end + +end + +--- Get free parking spots. +-- @param #FLIGHTCONTROL self +-- @param #number terminal Terminal type or nil. +-- @return #number Number of free spots. Total if terminal=nil or of the requested terminal type. +-- @return #table Table of free parking spots of data type #FLIGHCONTROL.ParkingSpot. +function FLIGHTCONTROL:_GetFreeParkingSpots(terminal) + + local freespots={} + + local n=0 + for _,_parking in pairs(self.parking) do + local parking=_parking --#FLIGHTCONTROL.ParkingSpot + + if parking.free then + if terminal==nil or terminal==parking.terminal then + n=n+1 + table.insert(freespots, parking) + end + end + end + + return n,freespots +end + +--- Init parking spots. +-- @param #FLIGHTCONTROL self +function FLIGHTCONTROL:_UpdateParkingSpots() + + -- Parking spots of airbase. + local parkingdata=self.airbase:GetParkingSpotsTable() + + local message="Parking Spots:" + for _,_parkingspot in pairs(self.parking) do + local parking=_parkingspot --#FLIGHTCONTROL.ParkingSpot + + for _,_spot in pairs(parkingdata) do + local spot=_spot --Wrapper.Airbase#AIRBASE.ParkingSpot + + if parking.id==spot.TerminalID then + + parking.position=spot.Coordinate + parking.drunway=spot.DistToRwy + parking.terminal=spot.TerminalType + parking.id=spot.TerminalID + parking.free=spot.Free + parking.reserved=spot.TOAC + + -- Mark position. + if parking.markerid then + parking.position:RemoveMark(parking.markerid) + end + + local text=string.format("ID=%d, Terminal=%d, Free=%s, Reserved=%s, Dist=%.1f", parking.id, parking.terminal, tostring(parking.free), tostring(parking.reserved), parking.drunway) + message=message.."\n"..text + parking.markerid=parking.position:MarkToAll(text) + + break + end + + end + end + + self:E(self.lid..message) +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Event handler for event birth. +-- @param #FLIGHTCONTROL self +-- @param Core.Event#EVENTDATA EventData +function FLIGHTCONTROL:OnEventBirth(EventData) + self:F3({EvendData=EventData}) + + self:T2(self.lid..string.format("BIRTH: unit = %s", tostring(EventData.IniUnitName))) + self:T2(self.lid..string.format("BIRTH: group = %s", tostring(EventData.IniGroupName))) + +end + +--- Event handler for event land. +-- @param #FLIGHTCONTROL self +-- @param Core.Event#EVENTDATA EventData +function FLIGHTCONTROL:OnEventLand(EventData) + self:F3({EvendData=EventData}) + + self:T2(self.lid..string.format("LAND: unit = %s", tostring(EventData.IniUnitName))) + self:T2(self.lid..string.format("LAND: group = %s", tostring(EventData.IniGroupName))) + + + -- This would be the closest airbase. + local airbase=EventData.Place + + -- Nil check for airbase. Crashed as player gave me no airbase. + if airbase==nil then + return + end + + -- Get airbase name. + local airbasename=tostring(airbase:GetName()) + + -- Check if landed at this airbase. + if airbasename==self.airbasename then + + -- AI always lands ==> remove unit from flight group and queues. + local flight=self:_ElementRecovered(EventData.IniUnit) + + if flight then + + -- Check if everybody is home. + local recovered=true + for _,_elem in pairs(flight.elements) do + local element=_elem --#FLIGHTCONTROL.FlightElement + if not element.recovered then + recovered=false + end + end + + -- Remove flight. + if recovered then + self:_RemoveFlightFromQueue(self.Qlanding, flight) + end + + end + + end + +end + +--- Event handler for event takeoff. +-- @param #FLIGHTCONTROL self +-- @param Core.Event#EVENTDATA EventData +function FLIGHTCONTROL:OnEventTakeoff(EventData) + self:F3({EvendData=EventData}) + + self:T2(self.lid..string.format("TAKEOFF: unit = %s", tostring(EventData.IniUnitName))) + self:T2(self.lid..string.format("TAKEOFF: group = %s", tostring(EventData.IniGroupName))) + + -- This would be the closest airbase. + local airbase=EventData.Place + + -- Unit that took off. + local unit=EventData.IniUnit + + -- Nil check for airbase. Crashed as player gave me no airbase. + if not (airbase or unit) then + self:E(self.lid.."WARNING: Airbase or IniUnit is nil in takeoff event!") + return + end + + -- Get airbase name. + local airbasename=tostring(airbase:GetName()) + + -- Check if landed at this airbase. + if airbasename==self.airbasename then + + -- AI always lands ==> remove unit from flight group and queues. + local flight=self:_ElementTookOff(unit) + + if flight then + + self:T(self.lid..string.format("Flight element %s took off.", EventData.IniUnitName)) + + -- Check if everybody is in the air. + local all=true + for _,_elem in pairs(flight.elements) do + local element=_elem --#FLIGHTCONTROL.FlightElement + if not element.tookoff then + all=false + end + end + + -- Remove flight. + if all then + self:T(self.lid..string.format("Flight group %s took off.", flight.groupname)) + self:_RemoveFlightFromQueue(self.Qtakeoff, flight) + end + + end + end +end + +--- Event handler for event engine startup. +-- @param #FLIGHTCONTROL self +-- @param Core.Event#EVENTDATA EventData +function FLIGHTCONTROL:OnEventEngineStartup(EventData) + self:F3({EvendData=EventData}) + + self:T2(self.lid..string.format("ENGINESTARTUP: unit = %s", tostring(EventData.IniUnitName))) + self:T2(self.lid..string.format("ENGINESTARTUP: group = %s", tostring(EventData.IniGroupName))) + + -- Unit that took off. + local unit=EventData.IniUnit + + -- Nil check for unit. + if not unit then + return + end + + -- Get flight element. + local element, idx, flight=self:_GetFlightElement(EventData.IniUnitName) + + if element then + local parkingspot=self:_GetElementParkingSpot(element) + if parkingspot then + self:_AddFlightToTakeoffQueue(flight) + end + end + +end + +--- Event handler for event engine shutdown. +-- @param #FLIGHTCONTROL self +-- @param Core.Event#EVENTDATA EventData +function FLIGHTCONTROL:OnEventEngineShutdown(EventData) + self:F3({EvendData=EventData}) + + self:T2(self.lid..string.format("ENGINESHUTDOWN: unit = %s", tostring(EventData.IniUnitName))) + self:T2(self.lid..string.format("ENGINESHUTDOWN: group = %s", tostring(EventData.IniGroupName))) + + -- Unit that took off. + local unit=EventData.IniUnit + + -- Nil check for unit. + if not unit then + return + end + +end + +--- Event handler for event crash. +-- @param #FLIGHTCONTROL self +-- @param Core.Event#EVENTDATA EventData +function FLIGHTCONTROL:OnEventEngineCrash(EventData) + self:F3({EvendData=EventData}) + + self:T2(self.lid..string.format("CRASH: unit = %s", tostring(EventData.IniUnitName))) + self:T2(self.lid..string.format("CRASH: group = %s", tostring(EventData.IniGroupName))) + + -- Unit that took off. + local unit=EventData.IniUnit + + -- Nil check for unit. + if not unit then + return + end + + self:_RemoveFlightElement(EventData.IniUnitName) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Queue Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Scan airbase zone. +-- @param #FLIGHTCONTROL self +function FLIGHTCONTROL:_CheckQueues() + + -- Print queues + self:_PrintQueue(self.flights, "All flights") + self:_PrintQueue(self.Qwaiting, "Waiting") + self:_PrintQueue(self.Qtakeoff, "Takeoff") + self:_PrintQueue(self.Qparking, "Parking") + + -- Get next wairing flight. + local flight=self:_GetNextWaitingFight() + + -- Number of groups landing. + local nlanding=#self.Qlanding + + -- Number of groups taking off. + local ntakeoff=#self.Qtakeoff + + if flight and nlanding==0 and ntakeoff==0 then + self:_LandAI(flight) + self:_RemoveFlightFromQueue(self.Qwaiting, flight) + end + +end + +--- Check parking spots. +-- @param #FLIGHTCONTROL self +function FLIGHTCONTROL:_CheckParking() + + -- Init all elements as NOT parking anywhere. + for _,_flight in pairs(self.flights) do + -- Loop over all elements. + for _,_element in pairs(_flight.elements) do + local element=_element --#FLIGHTCONTROL.FlightElement + element.parking=false + end + end + + for i,_spot in pairs(self.parking) do + local spot=_spot --#FLIGHTCONTROL.ParkingSpot + + -- Assume spot is free. + spot.free=true + + -- Loop over all flights. + for _,_flight in pairs(self.flights) do + + -- Loop over all elements. + for _,_element in pairs(_flight.elements) do + local element=_element --#FLIGHTCONTROL.FlightElement + + local unit=element.unit + + if unit and unit:IsAlive() then + + -- Distance to parking spot. + local dist=element.unit:GetCoordinate():Get3DDistance(spot.position) + + -- Element is parking on this spot + if dist<5 and not element.unit:InAir() then + element.parking=true + spot.free=false + end + + else + self:E(self.lid..string.format("ERROR: Element %s is not alive any more!", element.unitname)) + self:_RemoveFlightElement(element.unitname) + end + + end + + + end + end +end + +--- Get next flight waiting for landing clearance. +-- @param #FLIGHTCONTROL self +-- @return #FLIGHTCONTROL.FlightGroup Marshal flight next in line and ready to enter the pattern. Or nil if no flight is ready. +function FLIGHTCONTROL:_GetNextWaitingFight() + + -- Loop over all marshal flights. + for _,_flight in pairs(self.Qwaiting) do + local flight=_flight --#FLIGHTCONTROL.FlightGroup + + -- Current stack. + local stack=flight.flag + + -- Total marshal time in seconds. + local Tmarshal=timer.getAbsTime()-flight.time + + -- Min time in marshal stack. + local TmarshalMin=3*60 --Three minutes for human players. + + -- Check if conditions are right. + if flight.holding~=nil and Tmarshal>=TmarshalMin then + return flight + end + end + + return nil +end + +--- Print queue. +-- @param #FLIGHTCONTROL self +-- @param #table queue Queue to print. +-- @param #string name Queue name. +function FLIGHTCONTROL:_PrintQueue(queue, name) + + local text=string.format("%s Queue N=%d:", name, #queue) + if #queue==0 then + -- Queue is empty. + text=text.." empty." + else + + -- Loop over all flights in queue. + for i,_flight in ipairs(queue) do + local flight=_flight --#FLIGHTCONTROL.FlightGroup + + -- Gather info. + local clock=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) + local fuel=flight.group:GetFuelMin()*100 + local ai=tostring(flight.ai) + local actype=flight.actype + local holding=tostring(flight.holding) + local nunits=flight.nunits + + -- Main info. + text=text..string.format("\n[%d] %s (%s*%d): ai=%s, timestamp=%s, fuel=%d, inzone=%s, holding=%s", + i, flight.groupname, actype, nunits, ai, clock, fuel, tostring(flight.inzone), holding) + + -- Elements info. + for j,_element in pairs(flight.elements) do + local element=_element --#FLIGHTCONTROL.FlightElement + local life=element.unit:GetLife() + local life0=element.unit:GetLife0() + text=text..string.format("\n (%d) %s (%s): ai=%s, parking=%s, recovered=%s, tookoff=%s, airborne=%s life=%.1f/%.1f", + j, element.onboard, element.unitname, tostring(element.ai), tostring(element.parking), tostring(element.recovered), tostring(element.tookoff), tostring(element.unit:InAir()), life, life0) + end + end + end + + -- Display text. + self:T(self.lid..text) +end + +--- Remove a flight group from a queue. +-- @param #FLIGHTCONTROL self +-- @param #table queue The queue from which the group will be removed. +-- @param #FLIGHTCONTROL.FlightGroup flight Flight group that will be removed from queue. +-- @return #boolean True, flight was in Queue and removed. False otherwise. +-- @return #number Table index of removed queue element or nil. +function FLIGHTCONTROL:_RemoveFlightFromQueue(queue, flight) + + -- Loop over all flights in group. + for i,_flight in pairs(queue) do + local qflight=_flight --#FLIGHTCONTROL.FlightGroup + + -- Check for name. + if qflight.groupname==flight.groupname then + self:T(self.lid..string.format("Removing flight group %s from queue.", flight.groupname)) + table.remove(queue, i) + return true, i + end + end + + return false, nil +end + +--- Sets flag recovered=true and tookoff=false for a flight element, which was successfully recovered (landed). +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Unit#UNIT unit The aircraft unit that was recovered. +-- @return #FLIGHTCONTROL.FlightGroup Flight group of element. +function FLIGHTCONTROL:_ElementRecovered(unit) + + -- Get element of flight. + local element, idx, flight=self:_GetFlightElement(unit:GetName()) --#FLIGHTCONTROL.FlightElement + + -- Nil check. Could be if a helo landed or something else we dont know! + if element then + element.recovered=true + element.tookoff=false + end + + return flight +end + +--- Set tookoff to true for the flight element. +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Unit#UNIT unit The aircraft unit that was recovered. +-- @return #FLIGHTCONTROL.FlightGroup Flight group of element. +function FLIGHTCONTROL:_ElementTookOff(unit) + + -- Get element of flight. + local element, idx, flight=self:_GetFlightElement(unit:GetName()) --#FLIGHTCONTROL.FlightElement + + -- Nil check. Could be if a helo landed or something else we dont know! + if element then + element.tookoff=true + end + + return flight +end + +--- Add flight to landing queue and set recovered to false for all elements of the flight and its section members. +-- @param #FLIGHTCONTROL self +-- @param #FLIGHTCONTROL.FlightGroup flight Flight group of element. +function FLIGHTCONTROL:_AddFlightToLandingQueue(flight) + + -- Add flight to table. + table.insert(self.Qlanding, flight) + + -- Set flag to -1. + flight.flag=-1 + + -- New time stamp for time in pattern. + flight.time=timer.getAbsTime() + + -- Init recovered switch. + flight.recovered=false + for _,elem in pairs(flight.elements) do + elem.recoverd=false + end + +end + +--- Add flight to takeoff queue. +-- @param #FLIGHTCONTROL self +-- @param #FLIGHTCONTROL.FlightGroup flight Flight group of element. +function FLIGHTCONTROL:_AddFlightToTakeoffQueue(flight) + + -- Check if already in queue. + if self:_InQueue(self.Qtakeoff,flight.group) then + return + end + + -- Add flight to table. + table.insert(self.Qtakeoff, flight) + + -- Set flag to -1. + flight.flag=-1 + + -- New time stamp for time in pattern. + flight.time=timer.getAbsTime() + + -- Init recovered switch. + flight.tookoff=false + for _,elem in pairs(flight.elements) do + elem.tookoff=false + end + +end + +--- Check if a group is in a queue. +-- @param #FLIGHTCONTROL self +-- @param #table queue The queue to check. +-- @param Wrapper.Group#GROUP group The group to be checked. +-- @return #boolean If true, group is in the queue. False otherwise. +function FLIGHTCONTROL:_InQueue(queue, group) + local name=group:GetName() + for _,_flight in pairs(queue) do + local flight=_flight --#FLIGHTCONTROL.FlightGroup + if name==flight.groupname then + return true + end + end + return false +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Flight and Element Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new flight group. Usually when a flight appears in the CCA. +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #FLIGHTCONTROL.FlightGroup Flight group. +function FLIGHTCONTROL:_CreateFlightGroup(group) + + -- Check if not already in flights + if self:_InQueue(self.flights, group) then + return + end + + -- Debug info. + self:T(self.lid..string.format("Creating new flight for group %s of aircraft type %s.", group:GetName(), group:GetTypeName())) + + -- New flight. + local flight={} --#FLIGHTCONTROL.FlightGroup + + -- Flight group name + local groupname=group:GetName() + local human, playername=self:_IsHuman(group) + + -- Queue table item. + flight.group=group + flight.groupname=group:GetName() + flight.nunits=#group:GetUnits() + flight.time=timer.getAbsTime() + flight.dist0=group:GetCoordinate():Get2DDistance(self:GetCoordinate()) + flight.flag=-100 + flight.ai=not human + flight.actype=group:GetTypeName() + flight.onboardnumbers=self:_GetOnboardNumbers(group) + flight.inzone=flight.group:IsCompletelyInZone(self.zoneAirbase) + flight.holding=nil + + -- Flight elements. + local text=string.format("Flight elements of group %s:", flight.groupname) + + flight.elements={} + local units=group:GetUnits() + for i,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + local element={} --#FLIGHTCONTROL.FlightElement + element.unit=unit + element.unitname=unit:GetName() + element.onboard=flight.onboardnumbers[element.unitname] + element.sizemax=unit:GetObjectSize() + element.ai=not self:_IsHumanUnit(unit) + + -- Debug text. + text=text..string.format("\n[%d] %s onboard #%s, AI=%s", i, element.unitname, tostring(element.onboard), tostring(element.ai)) + + -- Add to table. + table.insert(flight.elements, element) + end + self:T(self.lid..text) + + -- Onboard. + if flight.ai then + local onboard=flight.onboardnumbers[flight.seclead] + flight.onboard=onboard + else + flight.onboard=self:_GetOnboardNumberPlayer(group) + end + + -- Add to known flights. + table.insert(self.flights, flight) + + return flight +end + +--- Get flight from group. +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Group#GROUP group Group that will be removed from queue. +-- @param #table queue The queue from which the group will be removed. +-- @return #FLIGHTCONTROL.FlightGroup Flight group or nil. +-- @return #number Queue index or nil. +function FLIGHTCONTROL:_GetFlightFromGroup(group) + + if group then + + -- Group name + local name=group:GetName() + + -- Loop over all flight groups in queue + for i,_flight in pairs(self.flights) do + local flight=_flight --#FLIGHTCONTROL.FlightGroup + + if flight.groupname==name then + return flight, i + end + end + + self:T2(self.lid..string.format("WARNING: Flight group %s could not be found in queue.", name)) + end + + self:T2(self.lid..string.format("WARNING: Flight group could not be found in queue. Group is nil!")) + return nil, nil +end + +--- Get element of flight from its unit name. +-- @param #FLIGHTCONTROL self +-- @param #string unitname Name of the unit. +-- @return #FLIGHTCONTROL.FlightElement Element of the flight or nil. +-- @return #number Element index or nil. +-- @return #FLIGHTCONTROL.FlightGroup The Flight group or nil. +function FLIGHTCONTROL:_GetFlightElement(unitname) + + -- Get the unit. + local unit=UNIT:FindByName(unitname) + + -- Check if unit exists. + if unit then + + -- Get flight element from all flights. + local flight=self:_GetFlightFromGroup(unit:GetGroup()) + + -- Check if fight exists. + if flight then + + -- Loop over all elements in flight group. + for i,_element in pairs(flight.elements) do + local element=_element --#FLIGHTCONTROL.FlightElement + + if element.unit:GetName()==unitname then + return element, i, flight + end + end + + self:T2(self.lid..string.format("WARNING: Flight element %s could not be found in flight group.", unitname, flight.groupname)) + end + end + + return nil, nil, nil +end + +--- Remove element from flight. +-- @param #FLIGHTCONTROL self +-- @param #string unitname Name of the unit. +-- @return #boolean If true, element could be removed or nil otherwise. +function FLIGHTCONTROL:_RemoveFlightElement(unitname) + + -- Get table index. + local element,idx, flight=self:_GetFlightElement(unitname) + + if idx then + table.remove(flight.elements, idx) + return true + else + self:T("WARNING: Flight element could not be removed from flight group. Index=nil!") + return nil + end +end + +--- Get parking spot of flight element. +-- @param #FLIGHTCONTROL self +-- @param #FLIGHTCONTROL.FlightElement element Element of flight group. +-- @return #FLIGHTCONTROL.ParkingSpot Parking spot of flight element or nil. +function FLIGHTCONTROL:_GetElementParkingSpot(element) + + if element then + + -- Unit object. + local unit=element.unit + + if unit then + + local upos=unit:GetCoordinate() + + for _,_parkingspot in pairs(self.parking) do + local parkingspot=_parkingspot --#FLIGHTCONTROL.ParkingSpot + + -- Spot position. + local spos=parkingspot.position + + -- 3D distance from unit to spot. + local dist=spos:Get3DDistance(upos) + + -- Distance threshold 5 meters. + if dist<5 then + return parkingspot + end + + end + + end + end + + return nil +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Routing Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Scan airbase zone and find new flights. +-- @param #FLIGHTCONTROL self +function FLIGHTCONTROL:_CheckInbound() + + -- Airbase position. + local coord=self:GetCoordinate() + + -- Scan radius = 20 NM. + local RCCZ=UTILS.NMToMeters(20) + + -- Debug info. + self:T(self.lid..string.format("Scanning airbase zone. Radius=%.1f NM.", UTILS.MetersToNM(RCCZ))) + + -- Scan units in carrier zone. + local _,_,_,unitscan=coord:ScanObjects(RCCZ, true, false, false) + + -- Make a table with all groups currently in the CCA zone. + local insideZone={} + + for _,_unit in pairs(unitscan) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Necessary conditions to be met: + local airborne=unit:IsAir() + local inzone=unit:IsInZone(self.zoneAirbase) + local friendly=self:GetCoalition()==unit:GetCoalition() + + -- Check if this an aircraft and that it is airborne and closing in. + if airborne and inzone and friendly then + + local group=unit:GetGroup() + local groupname=group:GetName() + + -- Add group to table. + if insideZone[groupname]==nil then + insideZone[groupname]=group + end + + end + end + + -- Find new flights that are inside CCA. + for groupname,_group in pairs(insideZone) do + local group=_group --Wrapper.Group#GROUP + self:_CreateFlightGroup(group) + end + + for _,_flight in pairs(self.flights) do + local flight=_flight --#FLIGHTCONTROL.FlightGroup + + local inzone=flight.group:IsCompletelyInZone(self.zoneAirbase) + + if inzone and not flight.inzone then + flight.inzone=true + flight.dist0=flight.group:GetCoordinate():Get2DDistance(self:GetCoordinate()) + end + + -- Set currently in zone or not. + flight.inzone=inzone + end + + + for _,_flight in pairs(self.flights) do + local flight=_flight --#FLIGHTCONTROL.FlightGroup + + --TODO: Check if aircraft has a landing waypoint for this airbase. + + -- Get distance to carrier. + local dist=flight.group:GetCoordinate():Get2DDistance(self:GetCoordinate()) + + -- Close in distance. Is >0 if AC comes closer wrt to first detected distance d0. + local closein=flight.dist0-dist + + -- TODO refine for current case. + + -- Flight closed in. + if closein>UTILS.NMToMeters(5) and flight.group:IsAirborne(true) then + + -- Debug info. + self:T3(self.lid..string.format("AI flight group %s closed in by %.1f NM", flight.groupname, UTILS.MetersToNM(closein))) + + -- Send AI to orbit outside 10 NM zone and wait until the next Marshal stack is available. + if not (self:_InQueue(self.Qwaiting, flight.group) or self:_InQueue(self.Qlanding, flight.group)) then + self:_WaitAI(flight, 1, true) + end + + -- Break the loop to not have all flights at once! Spams the message screen. + break + end + + end + +end + +--- Command AI flight to orbit. +-- @param #FLIGHTCONTROL self +-- @param #FLIGHTCONTROL.FlightGroup flight Flight group. +-- @param #number stack Holding stack. +-- @param #boolean respawn If true respawn the group. Otherwise reset the mission task with new waypoints. +function FLIGHTCONTROL:_WaitAI(flight, stack, respawn) + + -- Set flag to something other than -100 and <0 + flight.flag=stack + + -- Add AI flight to waiting queue. + table.insert(self.Qwaiting, flight) + + -- Flight group name. + local group=flight.group + local groupname=flight.groupname + + ---------------- + -- Set Speeds -- + ---------------- + + -- Aircraft speed 274 knots TAS ~= 250 KIAS when orbiting the pattern. (Orbit expects m/s.) + local speedOrbitMps=UTILS.KnotsToMps(274) + + -- Orbit speed in km/h for waypoints. + local speedOrbitKmh=UTILS.KnotsToKmph(274) + + -- Aircraft speed 400 knots when transiting to holding zone. (Waypoint expects km/h.) + local speedTransit=UTILS.KnotsToKmph(370) + + --------------- + -- Waypoints -- + --------------- + + -- Waypoints array to be filled depending on case etc. + local wp={} + + -- Current position. Always good for as the first waypoint. + wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedTransit, {}, "Current Position") + + -- Task function when arriving at the holding zone. This will set flight.holding=true. + local TaskHolding=flight.group:TaskFunction("FLIGHTCONTROL._ReachedHoldingZone", self, flight) + + -- Holding point. + local holding=self:_GetHoldingpoint(flight) + local altitude=holding.pos0.y + local angels=UTILS.MetersToFeet(altitude)/1000 + + -- Set orbit task. + local TaskOrbit=group:TaskOrbit(holding.pos0, altitude, speedOrbitMps, holding.pos1) + + -- Orbit at waypoint. + wp[#wp+1]=holding.pos0:WaypointAirTurningPoint(nil, speedOrbitKmh, {TaskHolding, TaskOrbit}, string.format("Holding at Angels %d", angels)) + + -- Debug markers. + if self.Debug then + holding.pos0:MarkToAll(string.format("Waiting Orbit of flight %s at Angels %s", groupname, angels)) + end + + if respawn then + + -- This should clear the landing waypoints. + -- Note: This resets the weapons and the fuel state. But not the units fortunately. + + -- Get group template. + local Template=group:GetTemplate() + + -- Set route points. + Template.route.points=wp + + -- Respawn the group. + group=group:Respawn(Template, true) + + end + + -- Reinit waypoints. + group:WayPointInitialize(wp) + + -- Route group. + group:Route(wp, 1) + +end + +--- Tell AI to land at the airbase. Flight is added to the landing queue. +-- @param #FLIGHTCONTROL self +-- @param #FLIGHTCONTROL.FlightGroup flight Flight group. +function FLIGHTCONTROL:_LandAI(flight) + + -- Debug info. + self:T(self.lid..string.format("Landing AI flight %s.", flight.groupname)) + + -- Airbase position. + local airbase=self.airbase:GetCoordinate() + + -- Waypoints array. + local wp={} + + -- Current speed. + local CurrentSpeed=flight.group:GetVelocityKMH() + + -- Aircraft speed when flying the pattern. + local Speed=UTILS.KnotsToKmph(150) + + -- Current positon. + wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil, CurrentSpeed, {}, "Current position") + + + -- Get active runway. + local runway=self:_GetActiveRunway() + + -- TODO: make dependend on AC type helos etc. + + -- Approach point: 10 NN in direction of runway. + local papproach=runway.position:Translate(UTILS.NMToMeters(10), runway.direction):SetAltitude(1000) + papproach:MarkToAll("Approach Point") + + -- Approach waypoint. + wp[#wp+1]=papproach:WaypointAirTurningPoint(nil ,Speed, {}, "Final Approach") + + -- Landing waypoint. + wp[#wp+1]=airbase:WaypointAirLanding(Speed, self.airbase, nil, "Landing") + + -- Reinit waypoints. + flight.group:WayPointInitialize(wp) + + -- Route group. + flight.group:Route(wp, 1) + + -- Add flight to landing queue. + table.insert(self.Qlanding, flight) +end + +--- Function called when a group has reached the holding zone. +--@param Wrapper.Group#GROUP group Group that reached the holding zone. +--@param #FLIGHTCONTROL flightcontrol Flightcontrol object. +--@param #FLIGHTCONTROL.FlightGroup flight Flight group that has reached the holding zone. +function FLIGHTCONTROL._ReachedHoldingZone(group, flightcontrol, flight) + + -- Debug message. + local text=string.format("Flight %s reached holding zone.", group:GetName()) + MESSAGE:New(text,10):ToAllIf(flightcontrol.Debug) + flightcontrol:T(flightcontrol.lid..text) + + -- Debug mark. + if flightcontrol.Debug then + group:GetCoordinate():MarkToAll(text) + end + + -- Set holding flag true and set timestamp for marshal time check. + if flight then + flight.holding=true + flight.time=timer.getAbsTime() + end +end + +--- Get holding point. +-- @param #FLIGHTCONTROL self +-- @param #FLIGHTCONTROL.FlightGroup flight Flight group. +-- @return #FLIGHTCONTROL.HoldingPoint Holding point. +function FLIGHTCONTROL:_GetHoldingpoint(flight) + + local holdingpoint={} --#FLIGHTCONTROL.HoldingPoint + + local runway=self:_GetActiveRunway() + + local hdg=runway.direction+90 + local dx=UTILS.NMToMeters(5) + local dz=UTILS.NMToMeters(1) + + local angels=UTILS.FeetToMeters(math.random(6,10)*1000) + + holdingpoint.pos0=runway.position:Translate(dx, hdg):SetAltitude(angels) + holdingpoint.pos1=holdingpoint.pos0:Translate(dz, runway.direction):SetAltitude(angels) + + return holdingpoint +end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--TODO: Get landing waypoint to check whether flight is meant to land at this airbase. + +--- Get coordinate of the airbase. +-- @param #FLIGHTCONTROL self +-- @return Core.Point#COORDINATE Coordinate of the airbase. +function FLIGHTCONTROL:GetCoordinate() + return self.airbase:GetCoordinate() +end + +--- Get coalition of the airbase. +-- @param #FLIGHTCONTROL self +-- @return #number Coalition ID. +function FLIGHTCONTROL:GetCoalition() + return self.airbase:GetCoalition() +end + +--- Get onboard number of player or client. +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #string Onboard number as string. +function FLIGHTCONTROL:_GetOnboardNumberPlayer(group) + return self:_GetOnboardNumbers(group, true) +end + +--- Get onboard numbers of all units in a group. +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @param #boolean playeronly If true, return the onboard number for player or client skill units. +-- @return #table Table of onboard numbers. +function FLIGHTCONTROL:_GetOnboardNumbers(group, playeronly) + + -- Get group name. + local groupname=group:GetName() + + -- Debug text. + local text=string.format("Onboard numbers of group %s:", groupname) + + -- Units of template group. + local units=group:GetTemplate().units + + -- Get numbers. + local numbers={} + for _,unit in pairs(units) do + + -- Onboard number and unit name. + local n=tostring(unit.onboard_num) + local name=unit.name + local skill=unit.skill + + -- Debug text. + text=text..string.format("\n- unit %s: onboard #=%s skill=%s", name, n, skill) + + if playeronly and skill=="Client" or skill=="Player" then + -- There can be only one player in the group, so we skip everything else. + return n + end + + -- Table entry. + numbers[name]=n + end + + -- Debug info. + self:T2(self.lid..text) + + return numbers +end + +--- Checks if a human player sits in the unit. +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @return #boolean If true, human player inside the unit. +function FLIGHTCONTROL:_IsHumanUnit(unit) + + -- Get player unit or nil if no player unit. + local playerunit=self:_GetPlayerUnitAndName(unit:GetName()) + + if playerunit then + return true + else + return false + end +end + +--- Checks if a group has a human player. +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #boolean If true, human player inside group. +function FLIGHTCONTROL:_IsHuman(group) + + -- Get all units of the group. + local units=group:GetUnits() + + -- Loop over all units. + for _,_unit in pairs(units) do + -- Check if unit is human. + local human=self:_IsHumanUnit(_unit) + if human then + return true + end + end + + return false +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #FLIGHTCONTROL self +-- @param #string _unitName Name of the player unit. +-- @return Wrapper.Unit#UNIT Unit of player or nil. +-- @return #string Name of the player or nil. +function FLIGHTCONTROL:_GetPlayerUnitAndName(_unitName) + self:F2(_unitName) + + if _unitName ~= nil then + + -- Get DCS unit from its name. + local DCSunit=Unit.getByName(_unitName) + + if DCSunit then + + local playername=DCSunit:getPlayerName() + local unit=UNIT:Find(DCSunit) + + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) + if DCSunit and unit and playername then + return unit, playername + end + + end + + end + + -- Return nil if we could not find a player. + return nil,nil +end + + + diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 400c2d3be..950218de5 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -655,6 +655,21 @@ function POSITIONABLE:GetVelocityVec3() return nil end +--- Get relative velocity with respect to another POSITIONABLE. +-- @param #POSITIONABLE self +-- @param #POSITIONABLE positionable Other positionable. +-- @return #number Relative velocity in m/s. +function POSITIONABLE:GetRelativeVelocity(positionable) + self:F2( self.PositionableName ) + + local v1=self:GetVelocityVec3() + local v2=positionable:GetVelocityVec3() + + local vtot=UTILS.VecAdd(v1,v2) + + return UTILS.VecNorm(vtot) +end + --- Returns the POSITIONABLE height in meters. -- @param Wrapper.Positionable#POSITIONABLE self diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index b1da22f5c..eaac2d480 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -58,10 +58,15 @@ Functional/Artillery.lua Functional/Suppression.lua Functional/PseudoATC.lua Functional/Warehouse.lua +Functional/RAT2.lua +Functional/RatCraft.lua +Functional/Fox2.lua +Functional/FlightModelData.lua Ops/Airboss.lua Ops/RecoveryTanker.lua Ops/RescueHelo.lua +Ops/FlightControl.lua AI/AI_Balancer.lua AI/AI_Air.lua