This commit is contained in:
Frank
2019-04-15 23:59:20 +02:00
parent 178ee09614
commit 274771b212
10 changed files with 3795 additions and 11 deletions
+1 -1
View File
@@ -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 } )
@@ -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/<Carrier>/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
+724
View File
@@ -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 dist<mindist then
mindist=dist
target=player.unit
end
end
end
-- Check if missile has a valid target.
if target then
local targetCoord=target:GetCoordinate()
local distance=missileCoord:Get3DDistance(targetCoord)
local bearing=targetCoord:HeadingTo(missileCoord)
local eta=distance/missileVelocity
self:T2(self.lid..string.format("Distance = %.1f m, v=%.1f m/s, bearing=%03d°, eta=%.1f sec", distance, missileVelocity, bearing, eta))
if distance<100 then
-- Destroy missile.
self:T(self.lid..string.format("Destroying missile at distance %.1f m", distance))
_ordnance:destroy()
-- Little explosion for the visual effect.
missileCoord:Explosion(10)
local text="Destroying missile. You're dead!"
MESSAGE:New(text, 10):ToGroup(target:GetGroup())
-- Terminate timer.
return nil
else
-- Time step.
local dt=1.0
if distance>50000 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<X> 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<X> 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
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+793
View File
@@ -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 nalive<ratcraft.ntot then
-- Try to spawn a ratcraft group.
local spawned=self:_TrySpawnRatcraft(ratcraft)
-- Remove queue item and break loop.
if spawned then
table.remove(self.Qspawn, i)
break
end
end
end
end
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Flightplan functions
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Calculate the maximum height an aircraft can reach for the given parameters.
-- @param #RAT2 self
-- @param #number D Total distance in meters from Departure to holding point at destination.
-- @param #number alphaC Climb angle in rad.
-- @param #number alphaD Descent angle in rad.
-- @param #number Hdep AGL altitude of departure point.
-- @param #number Hdest AGL altitude of destination point.
-- @param #number Deltahhold Relative altitude of holding point above destination.
-- @return #number Maximum height the aircraft can reach.
function RAT2:_GetMaxHeight(D, alphaC, alphaD, Hdep, Hdest, Deltahhold)
local Hhold=Hdest+Deltahhold
local hdest=Hdest-Hdep
local hhold=hdest+Deltahhold
local Dp=math.sqrt(D^2 + hhold^2)
local alphaS=math.atan(hdest/D) -- slope angle
local alphaH=math.atan(hhold/D) -- angle to holding point (could be necative!)
local alphaCp=alphaC-alphaH -- climb angle with slope
local alphaDp=alphaD+alphaH -- descent angle with slope
-- ASA triangle.
local gammap=math.pi-alphaCp-alphaDp
local sCp=Dp*math.sin(alphaDp)/math.sin(gammap)
local sDp=Dp*math.sin(alphaCp)/math.sin(gammap)
-- Max height from departure.
local hmax=sCp*math.sin(alphaC)
return hmax
end
--- Make a flight plan from a departure to a destination airport.
-- @param #RAT2 self
-- @param Functional.RatCraft#RATCRAFT ratcraft Ratcraft object.
-- @param Wrapper.Airbase#AIRBASE departure Departure airbase.
-- @param Wrapper.Airbase#AIRBASE destination Destination airbase.
-- @return #table Table of flightplan waypoints.
-- @return #table Table of flightplan coordinates.
function RAT2:_GetFlightplan(ratcraft, departure, destination)
-- Parameters in SI units (m/s, m).
local Vmax=ratcraft.speedmax/3.6
local Range=ratcraft.range
local category=ratcraft.category
local ceiling=ratcraft.DCSdesc.Hmax
local Vymax=ratcraft.DCSdesc.VyMax
-- Max cruise speed 90% of max speed.
local VxCruiseMax=0.90*Vmax
-- Min cruise speed 70% of max cruise or 600 km/h whichever is lower.
local VxCruiseMin = math.min(VxCruiseMax*0.70, 166)
-- Cruise speed (randomized). Expectation value at midpoint between min and max.
local VxCruise = UTILS.RandomGaussian((VxCruiseMax-VxCruiseMin)/2+VxCruiseMin, (VxCruiseMax-VxCruiseMax)/4, VxCruiseMin, VxCruiseMax)
-- Climb speed 90% ov Vmax but max 720 km/h.
local VxClimb = math.min(Vmax*0.90, 200)
-- Descent speed 60% of Vmax but max 500 km/h.
local VxDescent = math.min(Vmax*0.60, 140)
-- Holding speed is 90% of descent speed.
local VxHolding = VxDescent*0.9
-- Final leg is 90% of holding speed.
local VxFinal = VxHolding*0.9
-- Reasonably civil climb speed Vy=1500 ft/min = 7.6 m/s but max aircraft specific climb rate.
local VyClimb=math.min(7.6, Vymax)
-- Climb angle in rad.
--local AlphaClimb=math.asin(VyClimb/VxClimb)
local AlphaClimb=math.rad(4)
-- Descent angle in rad. Moderate 4 degrees.
local AlphaDescent=math.rad(4)
-- Expected cruise level (peak of Gaussian distribution)
local FLcruise_expect=150*RAT.unit.FL2m
if category==Group.Category.HELICOPTER then
FLcruise_expect=1000 -- 1000 m ASL
end
-------------------------
--- DEPARTURE AIRPORT ---
-------------------------
-- Coordinates of departure point.
local Pdeparture=departure:GetCoordinate()
-- Height ASL of departure point.
local H_departure=Pdeparture.y
---------------------------
--- DESTINATION AIRPORT ---
---------------------------
-- Position of destination airport.
local Pdestination=destination:GetCoordinate()
-- Height ASL of destination airport/zone.
local H_destination=Pdestination.y
-----------------------------
--- DESCENT/HOLDING POINT ---
-----------------------------
-- Get a random point between 5 and 10 km away from the destination.
local Rhmin=5000
local Rhmax=10000
-- For helos we set a distance between 500 to 1000 m.
if category==Group.Category.HELICOPTER then
Rhmin=500
Rhmax=1000
end
-- Coordinates of the holding point. y is the land height at that point.
local Pholding=Pdestination:GetRandomCoordinateInRadius(Rhmax, Rhmin)
-- Distance from holding point to final destination (not used).
local d_holding=Pholding:Get2DDistance(Pdestination)
-- AGL height of holding point.
local H_holding=Pholding.y
---------------
--- GENERAL ---
---------------
-- We go directly to the holding point not the destination airport. From there, planes are guided by DCS to final approach.
local heading=Pdeparture:HeadingTo(Pholding)
local d_total=Pdeparture:Get2DDistance(Pholding)
------------------------------
--- Holding Point Altitude ---
------------------------------
-- Holding point altitude. For planes between 1600 and 2400 m AGL. For helos 160 to 240 m AGL.
local h_holding=1200
if category==Group.Category.HELICOPTER then
h_holding=150
end
h_holding=UTILS.Randomize(h_holding, 0.2)
-- Max holding altitude.
local DeltaholdingMax=self:_GetMaxHeight(d_total, AlphaClimb, AlphaDescent, H_departure, H_holding, 0)
if h_holding>DeltaholdingMax 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_expect<FLmin then
FLcruise_expect=FLmin
end
if FLcruise_expect>FLmax 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: <alias>-01, <alias>-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
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+24 -10
View File
@@ -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.
@@ -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
+5
View File
@@ -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' )
File diff suppressed because it is too large Load Diff
@@ -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
+5
View File
@@ -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