Merge pull request #1112 from FlightControl-Master/FF/Develop

Carrier Ops
This commit is contained in:
Frank 2019-01-31 14:21:49 +01:00 committed by GitHub
commit 264e84649e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 692 additions and 252 deletions

View File

@ -7,7 +7,7 @@
-- ===
--
-- @module AI.AI_Air
-- @image AI_Air_Operations.JPG
-- @image MOOSE.JPG
--- @type AI_AIR
-- @extends Core.Fsm#FSM_CONTROLLABLE

View File

@ -325,6 +325,7 @@ function SPAWN:New( SpawnTemplatePrefix )
self.SpawnInitFreq = nil -- No special frequency.
self.SpawnInitModu = nil -- No special modulation.
self.SpawnInitRadio = nil -- No radio comms setting.
self.SpawnInitModex = nil
self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned.
else
@ -376,6 +377,7 @@ function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix )
self.SpawnInitFreq = nil -- No special frequency.
self.SpawnInitModu = nil -- No special modulation.
self.SpawnInitRadio = nil -- No radio comms setting.
self.SpawnInitModex = nil
self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned.
else
@ -430,6 +432,7 @@ function SPAWN:NewFromTemplate( SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPr
self.SpawnInitFreq = nil -- No special frequency.
self.SpawnInitModu = nil -- No special modulation.
self.SpawnInitRadio = nil -- No radio comms setting.
self.SpawnInitModex = nil
self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned.
else
@ -642,6 +645,19 @@ function SPAWN:InitRadioModulation(modulation)
return self
end
--- Sets the modex of the first unit of the group. If more units are in the group, the number is increased by one with every unit.
-- @param #SPAWN self
-- @param #number modex Modex of the first unit.
-- @return #SPAWN self
function SPAWN:InitModex(modex)
if modex then
self.SpawnInitModex=tonumber(modex)
end
return self
end
--- Randomizes the defined route of the SpawnTemplatePrefix group in the ME. This is very useful to define extra variation of the behaviour of groups.
-- @param #SPAWN self
@ -1219,6 +1235,12 @@ function SPAWN:SpawnWithIndex( SpawnIndex )
end
end
-- Set tail number.
if self.SpawnInitModex then
for UnitID = 1, #SpawnTemplate.units do
SpawnTemplate.units[UnitID].onboard_num = string.format("%03d", self.SpawnInitModex+(UnitID-1))
end
end
-- Set radio comms on/off.
if self.SpawnInitRadio then

View File

@ -11,7 +11,7 @@
--
-- ## Features:
--
-- * Impact points of bombs, rockets and missils are recorded and distance to closest range target is measured and reported to the player.
-- * Impact points of bombs, rockets and missiles are recorded and distance to closest range target is measured and reported to the player.
-- * Number of hits on strafing passes are counted and reported. Also the percentage of hits w.r.t fired shots is evaluated.
-- * Results of all bombing and strafing runs are stored and top 10 results can be displayed.
-- * Range targets can be marked by smoke.
@ -56,9 +56,9 @@
-- @field #table strafeStatus Table containing the current strafing target a player as assigned to.
-- @field #table strafePlayerResults Table containing the strafing results of each player.
-- @field #table bombPlayerResults Table containing the bombing results of each player.
-- @field #table PlayerSettings Indiviual player settings.
-- @field #table PlayerSettings Individual player settings.
-- @field #number dtBombtrack Time step [sec] used for tracking released bomb/rocket positions. Default 0.005 seconds.
-- @field #number BombtrackThreshold Bombs/rockets/missiles are only tracked if player-range distance is smaller than this threashold [m]. Default 25000 m.
-- @field #number BombtrackThreshold Bombs/rockets/missiles are only tracked if player-range distance is smaller than this threshold [m]. Default 25000 m.
-- @field #number Tmsg Time [sec] messages to players are displayed. Default 30 sec.
-- @field #string examinergroupname Name of the examiner group which should get all messages.
-- @field #boolean examinerexclusive If true, only the examiner gets messages. If false, clients and examiner get messages.
@ -75,10 +75,11 @@
-- @field #boolean trackbombs If true (default), all bomb types are tracked and impact point to closest bombing target is evaluated.
-- @field #boolean trackrockets If true (default), all rocket types are tracked and impact point to closest bombing target is evaluated.
-- @field #boolean trackmissiles If true (default), all missile types are tracked and impact point to closest bombing target is evaluated.
-- @field #boolean defaultsmokebomb If true, initialize player settings to smoke bomb.
-- @extends Core.Base#BASE
--- Enables a mission designer to easily set up practice ranges in DCS. A new RANGE object can be created with the @{#RANGE.New}(rangename) contructor.
-- The parameter "rangename" defindes the name of the range. It has to be unique since this is also the name displayed in the radio menu.
-- The parameter "rangename" defines the name of the range. It has to be unique since this is also the name displayed in the radio menu.
--
-- Generally, a range consists of strafe pits and bombing targets. For strafe pits the number of hits for each pass is counted and tabulated.
-- For bombing targets, the distance from the impact point of the bomb, rocket or missile to the closest range target is measured and tabulated.
@ -89,12 +90,12 @@
-- **IMPORTANT**
--
-- Due to a DCS bug, it is not possible to directly monitor when a player enters a plane. So in a mission with client slots, it is vital that
-- a player first enters as spector and **after that** jumps into the slot of his aircraft!
-- a player first enters as spectator or hits ESC twice and **after that** jumps into the slot of his aircraft!
-- If that is not done, the script is not started correctly. This can be checked by looking at the radio menues. If the mission was entered correctly,
-- there should be an "On the Range" menu items in the "F10. Other..." menu.
--
-- ## Strafe Pits
-- Each strafe pit can consist of multiple targets. Often one findes two or three strafe targets next to each other.
-- Each strafe pit can consist of multiple targets. Often one finds two or three strafe targets next to each other.
--
-- A strafe pit can be added to the range by the @{#RANGE.AddStrafePit}(*targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*) function.
--
@ -104,7 +105,7 @@
-- If the parameter *heading* is passed as **nil**, the heading is automatically taken from the heading of the first target unit as defined in the ME.
-- The parameter *inverseheading* turns the heading around by 180 degrees. This is sometimes useful, since the default heading of strafe target units point in the
-- wrong/opposite direction.
-- * The parameter *goodpass* defines the number of hits a pilot has to achive during a run to be judged as a "good" pass.
-- * The parameter *goodpass* defines the number of hits a pilot has to achieve during a run to be judged as a "good" pass.
-- * The last parameter *foulline* sets the distance from the pit targets to the foul line. Hit from closer than this line are not counted!
--
-- Another function to add a strafe pit is @{#RANGE.AddStrafePitGroup}(*group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*). Here,
@ -151,7 +152,7 @@
-- * "F2. My Settings": Player specific settings.
-- * "F3. Stats" Player: statistics and scores.
-- * "Range Information": Information about the range, such as bearing and range. Also range and player specific settings are displayed.
-- * "Weather Report": Temperatur, wind and QFE pressure information is provided.
-- * "Weather Report": Temperature, wind and QFE pressure information is provided.
--
-- ## Examples
--
@ -243,6 +244,7 @@ RANGE={
trackbombs=true,
trackrockets=true,
trackmissiles=true,
defaultsmokebomb=true,
}
--- Default range parameters.
@ -266,19 +268,25 @@ RANGE.Defaults={
-- @field #table Names
RANGE.Names={}
--- Main radio menu.
-- @field #table MenuF10
--- Main radio menu on group level.
-- @field #table MenuF10 Root menu table on group level.
RANGE.MenuF10={}
--- Main radio menu on mission level.
-- @field #table MenuF10Root Root menu on mission level.
RANGE.MenuF10Root=nil
--- Some ID to identify who we are in output of the DCS.log file.
-- @field #string id
RANGE.id="RANGE | "
--- Range script version.
-- @field #string version
RANGE.version="1.2.3"
RANGE.version="1.2.4"
--TODO list:
--TODO: Verbosity level for messages.
--TODO: Add option for default settings such as smoke off.
--TODO: Add custom weapons, which can be specified by the user.
--TODO: Check if units are still alive.
--DONE: Add statics for strafe pits.
@ -311,99 +319,111 @@ function RANGE:New(rangename)
self:E(RANGE.id..text)
MESSAGE:New(text, 10):ToAllIf(self.Debug)
-- Defaults
self:SetDefaultPlayerSmokeBomb()
-- Return object.
return self
end
--- Initializes number of targets and location of the range. Starts the event handlers.
-- @param #RANGE self
function RANGE:Start()
-- @param #number delay Delay in seconds, before the RANGE is started. Default immediately.
-- @return self
function RANGE:Start(delay)
self:F()
-- Location/coordinate of range.
local _location=nil
if delay and delay>0 then
SCHEDULER:New(nil, self.Start, {self}, delay)
else
-- Count bomb targets.
local _count=0
for _,_target in pairs(self.bombingTargets) do
_count=_count+1
-- Location/coordinate of range.
local _location=nil
-- Get range location.
if _location==nil then
_location=_target.target:GetCoordinate() --Core.Point#COORDINATE
end
end
self.nbombtargets=_count
-- Count bomb targets.
local _count=0
for _,_target in pairs(self.bombingTargets) do
_count=_count+1
-- Count strafing targets.
_count=0
for _,_target in pairs(self.strafeTargets) do
_count=_count+1
for _,_unit in pairs(_target.targets) do
-- Get range location.
if _location==nil then
_location=_unit:GetCoordinate()
_location=_target.target:GetCoordinate() --Core.Point#COORDINATE
end
end
end
self.nstrafetargets=_count
self.nbombtargets=_count
-- Location of the range. We simply take the first unit/target we find if it was not explicitly specified by the user.
if self.location==nil then
self.location=_location
end
-- Count strafing targets.
_count=0
for _,_target in pairs(self.strafeTargets) do
_count=_count+1
if self.location==nil then
local text=string.format("ERROR! No range location found. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets)
self:E(RANGE.id..text)
return
end
for _,_unit in pairs(_target.targets) do
if _location==nil then
_location=_unit:GetCoordinate()
end
end
end
self.nstrafetargets=_count
-- Define a MOOSE zone of the range.
if self.rangezone==nil then
self.rangezone=ZONE_RADIUS:New(self.rangename, {x=self.location.x, y=self.location.z}, self.rangeradius)
end
-- Location of the range. We simply take the first unit/target we find if it was not explicitly specified by the user.
if self.location==nil then
self.location=_location
end
-- Starting range.
local text=string.format("Starting RANGE %s. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets)
self:E(RANGE.id..text)
MESSAGE:New(text,10):ToAllIf(self.Debug)
if self.location==nil then
local text=string.format("ERROR! No range location found. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets)
self:E(RANGE.id..text)
return
end
-- Event handling.
if self.eventmoose then
-- Events are handled my MOOSE.
self:T(RANGE.id.."Events are handled by MOOSE.")
self:HandleEvent(EVENTS.Birth)
self:HandleEvent(EVENTS.Hit)
self:HandleEvent(EVENTS.Shot)
else
-- Events are handled directly by DCS.
self:T(RANGE.id.."Events are handled directly by DCS.")
world.addEventHandler(self)
end
-- Define a MOOSE zone of the range.
if self.rangezone==nil then
self.rangezone=ZONE_RADIUS:New(self.rangename, {x=self.location.x, y=self.location.z}, self.rangeradius)
end
-- Make bomb target move randomly within the range zone.
for _,_target in pairs(self.bombingTargets) do
-- Starting range.
local text=string.format("Starting RANGE %s. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets)
self:I(RANGE.id..text)
MESSAGE:New(text,10):ToAllIf(self.Debug)
-- Check if it is a static object.
local _static=self:_CheckStatic(_target.target:GetName())
-- Event handling.
if self.eventmoose then
-- Events are handled my MOOSE.
self:T(RANGE.id.."Events are handled by MOOSE.")
self:HandleEvent(EVENTS.Birth)
self:HandleEvent(EVENTS.Hit)
self:HandleEvent(EVENTS.Shot)
else
-- Events are handled directly by DCS.
self:T(RANGE.id.."Events are handled directly by DCS.")
world.addEventHandler(self)
end
if _target.move and _static==false and _target.speed>1 then
local unit=_target.target --Wrapper.Unit#UNIT
_target.target:PatrolZones({self.rangezone}, _target.speed*0.75, "Off road")
-- Make bomb target move randomly within the range zone.
for _,_target in pairs(self.bombingTargets) do
-- Check if it is a static object.
local _static=self:_CheckStatic(_target.target:GetName())
if _target.move and _static==false and _target.speed>1 then
local unit=_target.target --Wrapper.Unit#UNIT
_target.target:PatrolZones({self.rangezone}, _target.speed*0.75, "Off road")
end
end
-- Debug mode: smoke all targets and range zone.
if self.Debug then
self:_MarkTargetsOnMap()
self:_SmokeBombTargets()
self:_SmokeStrafeTargets()
self:_SmokeStrafeTargetBoxes()
self.rangezone:SmokeZone(SMOKECOLOR.White)
end
end
-- Debug mode: smoke all targets and range zone.
if self.Debug then
self:_MarkTargetsOnMap()
self:_SmokeBombTargets()
self:_SmokeStrafeTargets()
self:_SmokeStrafeTargetBoxes()
self.rangezone:SmokeZone(SMOKECOLOR.White)
end
return self
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@ -412,144 +432,199 @@ end
--- Set maximal strafing altitude. Player entering a strafe pit above that altitude are not registered for a valid pass.
-- @param #RANGE self
-- @param #number maxalt Maximum altitude AGL in meters. Default is 914 m= 3000 ft.
-- @return #RANGE self
function RANGE:SetMaxStrafeAlt(maxalt)
self.strafemaxalt=maxalt or RANGE.Defaults.strafemaxalt
return self
end
--- Set time interval for tracking bombs. A smaller time step increases accuracy but needs more CPU time.
-- @param #RANGE self
-- @param #number dt Time interval in seconds. Default is 0.005 s.
-- @return #RANGE self
function RANGE:SetBombtrackTimestep(dt)
self.dtBombtrack=dt or RANGE.Defaults.dtBombtrack
return self
end
--- Set time how long (most) messages are displayed.
-- @param #RANGE self
-- @param #number time Time in seconds. Default is 30 s.
-- @return #RANGE self
function RANGE:SetMessageTimeDuration(time)
self.Tmsg=time or RANGE.Defaults.Tmsg
return self
end
--- Set messages to examiner. The examiner will receive messages from all clients.
-- @param #RANGE self
-- @param #string examinergroupname Name of the group of the examiner.
-- @param #boolean exclusively If true, messages are send exclusively to the examiner, i.e. not to the clients.
-- @return #RANGE self
function RANGE:SetMessageToExaminer(examinergroupname, exclusively)
self.examinergroupname=examinergroupname
self.examinerexclusive=exclusively
return self
end
--- Set max number of player results that are displayed.
-- @param #RANGE self
-- @param #number nmax Number of results. Default is 10.
-- @return #RANGE self
function RANGE:SetDisplayedMaxPlayerResults(nmax)
self.ndisplayresult=nmax or RANGE.Defaults.ndisplayresult
return self
end
--- Set range radius. Defines the area in which e.g. bomb impacts are smoked.
-- @param #RANGE self
-- @param #number radius Radius in km. Default 5 km.
-- @return #RANGE self
function RANGE:SetRangeRadius(radius)
self.rangeradius=radius*1000 or RANGE.Defaults.rangeradius
return self
end
--- Set player setting whether bomb impact points are smoked or not
-- @param #RANGE self
-- @param #boolean If true nor nil default is to smoke impact points of bombs.
-- @return #RANGE self
function RANGE:SetDefaultPlayerSmokeBomb(switch)
if switch==true or switch==nil then
self.defaultsmokebomb=true
else
self.defaultsmokebomb=false
end
return self
end
--- Set bomb track threshold distance. Bombs/rockets/missiles are only tracked if player-range distance is less than this distance. Default 25 km.
-- @param #RANGE self
-- @param #number distance Threshold distance in km. Default 25 km.
-- @return #RANGE self
function RANGE:SetBombtrackThreshold(distance)
self.BombtrackThreshold=distance*1000 or 25*1000
return self
end
--- Set range location. If this is not done, one (random) unit position of the range is used to determine the location of the range.
-- The range location determines the position at which the weather data is evaluated.
-- @param #RANGE self
-- @param Core.Point#COORDINATE coordinate Coordinate of the range.
-- @return #RANGE self
function RANGE:SetRangeLocation(coordinate)
self.location=coordinate
return self
end
--- Set range zone. For example, no bomb impact points are smoked if a bomb falls outside of this zone.
-- If a zone is not explicitly specified, the range zone is determined by its location and radius.
-- @param #RANGE self
-- @param Core.Zone#ZONE zone MOOSE zone defining the range perimeters.
-- @return #RANGE self
function RANGE:SetRangeZone(zone)
self.rangezone=zone
return self
end
--- Set smoke color for marking bomb targets. By default bomb targets are marked by red smoke.
-- @param #RANGE self
-- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.Red.
-- @return #RANGE self
function RANGE:SetBombTargetSmokeColor(colorid)
self.BombSmokeColor=colorid or SMOKECOLOR.Red
return self
end
--- Set smoke color for marking strafe targets. By default strafe targets are marked by green smoke.
-- @param #RANGE self
-- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.Green.
-- @return #RANGE self
function RANGE:SetStrafeTargetSmokeColor(colorid)
self.StrafeSmokeColor=colorid or SMOKECOLOR.Green
return self
end
--- Set smoke color for marking strafe pit approach boxes. By default strafe pit boxes are marked by white smoke.
-- @param #RANGE self
-- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.White.
-- @return #RANGE self
function RANGE:SetStrafePitSmokeColor(colorid)
self.StrafePitSmokeColor=colorid or SMOKECOLOR.White
return self
end
--- Set time delay between bomb impact and starting to smoke the impact point.
-- @param #RANGE self
-- @param #number delay Time delay in seconds. Default is 3 seconds.
-- @return #RANGE self
function RANGE:SetSmokeTimeDelay(delay)
self.TdelaySmoke=delay or RANGE.Defaults.TdelaySmoke
return self
end
--- Enable debug modus.
-- @param #RANGE self
-- @return #RANGE self
function RANGE:DebugON()
self.Debug=true
return self
end
--- Disable debug modus.
-- @param #RANGE self
-- @return #RANGE self
function RANGE:DebugOFF()
self.Debug=false
return self
end
--- Enables tracking of all bomb types. Note that this is the default setting.
-- @param #RANGE self
-- @return #RANGE self
function RANGE:TrackBombsON()
self.trackbombs=true
return self
end
--- Disables tracking of all bomb types.
-- @param #RANGE self
-- @return #RANGE self
function RANGE:TrackBombsOFF()
self.trackbombs=false
return self
end
--- Enables tracking of all rocket types. Note that this is the default setting.
-- @param #RANGE self
-- @return #RANGE self
function RANGE:TrackRocketsON()
self.trackrockets=true
return self
end
--- Disables tracking of all rocket types.
-- @param #RANGE self
-- @return #RANGE self
function RANGE:TrackRocketsOFF()
self.trackrockets=false
return self
end
--- Enables tracking of all missile types. Note that this is the default setting.
-- @param #RANGE self
-- @return #RANGE self
function RANGE:TrackMissilesON()
self.trackmissiles=true
return self
end
--- Disables tracking of all missile types.
-- @param #RANGE self
-- @return #RANGE self
function RANGE:TrackMissilesOFF()
self.trackmissiles=false
return self
end
@ -564,6 +639,7 @@ end
-- @param #boolean inverseheading (Optional) Take inverse heading (heading --> heading - 180 Degrees). Default is false.
-- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20.
-- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default 610 m = 2000 ft. Set to 0 for no foul line.
-- @return #RANGE self
function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline)
self:F({targetnames=targetnames, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline})
@ -681,6 +757,8 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe
local text=string.format("Adding new strafe target %s with %d targets: heading = %03d, box_L = %.1f, box_W = %.1f, goodpass = %d, foul line = %.1f", _name, ntargets, heading, l, w, goodpass, foulline)
self:T(RANGE.id..text)
MESSAGE:New(text, 5):ToAllIf(self.Debug)
return self
end
@ -696,6 +774,7 @@ end
-- @param #boolean inverseheading (Optional) Take inverse heading (heading --> heading - 180 Degrees). Default is false.
-- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20.
-- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default 610 m = 2000 ft. Set to 0 for no foul line.
-- @return #RANGE self
function RANGE:AddStrafePitGroup(group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline)
self:F({group=group, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline})
@ -721,6 +800,7 @@ function RANGE:AddStrafePitGroup(group, boxlength, boxwidth, heading, inversehea
self:AddStrafePit(_names, boxlength, boxwidth, heading, inverseheading, goodpass, foulline)
end
return self
end
--- Add bombing target(s) to range.
@ -728,6 +808,7 @@ end
-- @param #table targetnames Table containing names of unit or static objects serving as bomb targets.
-- @param #number goodhitrange (Optional) Max distance from target unit (in meters) which is considered as a good hit. Default is 25 m.
-- @param #boolean randommove If true, unit will move randomly within the range. Default is false.
-- @return #RANGE self
function RANGE:AddBombingTargets(targetnames, goodhitrange, randommove)
self:F({targetnames=targetnames, goodhitrange=goodhitrange, randommove=randommove})
@ -757,6 +838,8 @@ function RANGE:AddBombingTargets(targetnames, goodhitrange, randommove)
end
end
return self
end
--- Add a unit or static object as bombing target.
@ -764,6 +847,7 @@ end
-- @param Wrapper.Positionable#POSITIONABLE unit Positionable (unit or static) of the strafe target.
-- @param #number goodhitrange Max distance from unit which is considered as a good hit.
-- @param #boolean randommove If true, unit will move randomly within the range. Default is false.
-- @return #RANGE self
function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove)
self:F({unit=unit, goodhitrange=goodhitrange, randommove=randommove})
@ -798,6 +882,8 @@ function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove)
-- Insert target to table.
table.insert(self.bombingTargets, {name=name, target=unit, goodhitrange=goodhitrange, move=randommove, speed=speed})
return self
end
--- Add all units of a group as bombing targets.
@ -805,6 +891,7 @@ end
-- @param Wrapper.Group#GROUP group Group of bombing targets.
-- @param #number goodhitrange Max distance from unit which is considered as a good hit.
-- @param #boolean randommove If true, unit will move randomly within the range. Default is false.
-- @return #RANGE self
function RANGE:AddBombingTargetGroup(group, goodhitrange, randommove)
self:F({group=group, goodhitrange=goodhitrange, randommove=randommove})
@ -819,6 +906,7 @@ function RANGE:AddBombingTargetGroup(group, goodhitrange, randommove)
end
end
return self
end
--- Measures the foule line distance between two unit or static objects.
@ -971,11 +1059,12 @@ function RANGE:OnEventBirth(EventData)
-- By default, some bomb impact points and do not flare each hit on target.
self.PlayerSettings[_playername]={}
self.PlayerSettings[_playername].smokebombimpact=true
self.PlayerSettings[_playername].smokebombimpact=self.defaultsmokebomb
self.PlayerSettings[_playername].flaredirecthits=false
self.PlayerSettings[_playername].smokecolor=SMOKECOLOR.Blue
self.PlayerSettings[_playername].flarecolor=FLARECOLOR.Red
self.PlayerSettings[_playername].delaysmoke=true
self.PlayerSettings[_playername].messages=true
-- Start check in zone timer.
if self.planes[_uid] ~= true then
@ -1042,7 +1131,7 @@ function RANGE:OnEventHit(EventData)
if _currentTarget.pastfoulline==false and _unit and _playername then
local _d=_currentTarget.zone.foulline
local text=string.format("%s, Invalid hit!\nYou already passed foul line distance of %d m for target %s.", self:_myname(_unitName), _d, targetname)
self:_DisplayMessageToGroup(_unit, text, 10)
self:_DisplayMessageToGroup(_unit, text)
self:T2(RANGE.id..text)
_currentTarget.pastfoulline=true
end
@ -1320,7 +1409,7 @@ function RANGE:_DisplayMyStrafePitResults(_unitName)
end
-- Send message to group.
self:_DisplayMessageToGroup(_unit, _message, nil, true)
self:_DisplayMessageToGroup(_unit, _message, nil, true, true)
end
end
@ -1376,7 +1465,7 @@ function RANGE:_DisplayStrafePitResults(_unitName)
end
-- Send message.
self:_DisplayMessageToGroup(_unit, _message, nil, true)
self:_DisplayMessageToGroup(_unit, _message, nil, true, true)
end
end
@ -1433,7 +1522,7 @@ function RANGE:_DisplayMyBombingResults(_unitName)
end
-- Send message.
self:_DisplayMessageToGroup(_unit, _message, nil, true)
self:_DisplayMessageToGroup(_unit, _message, nil, true, true)
end
end
@ -1489,7 +1578,7 @@ function RANGE:_DisplayBombingResults(_unitName)
end
-- Send message.
self:_DisplayMessageToGroup(_unit, _message, nil, true)
self:_DisplayMessageToGroup(_unit, _message, nil, true, true)
end
end
@ -1566,7 +1655,7 @@ function RANGE:_DisplayRangeInfo(_unitname)
text=text..textdelay
-- Send message to player group.
self:_DisplayMessageToGroup(unit, text, nil, true)
self:_DisplayMessageToGroup(unit, text, nil, true, true)
-- Debug output.
self:T2(RANGE.id..text)
@ -1603,7 +1692,7 @@ function RANGE:_DisplayBombTargets(_unitname)
end
end
self:_DisplayMessageToGroup(_unit,_text, nil, true)
self:_DisplayMessageToGroup(_unit,_text, nil, true, true)
end
end
@ -1643,7 +1732,7 @@ function RANGE:_DisplayStrafePits(_unitname)
_text=_text..string.format("\n- %s: %s - heading %03d",_strafepit.name, mycoord, heading)
end
self:_DisplayMessageToGroup(_unit,_text, nil, true)
self:_DisplayMessageToGroup(_unit,_text, nil, true, true)
end
end
@ -1705,7 +1794,7 @@ function RANGE:_DisplayRangeWeather(_unitname)
end
-- Send message to player group.
self:_DisplayMessageToGroup(unit, text, nil, true)
self:_DisplayMessageToGroup(unit, text, nil, true, true)
-- Debug output.
self:T2(RANGE.id..text)
@ -1749,7 +1838,7 @@ function RANGE:_CheckInZone(_unitName)
local unitinzone=_unit:IsInZone(zone) and unitalt <= self.strafemaxalt and towardspit
-- Debug output
local text=string.format("Checking stil in zone. Unit = %s, player = %s in zone = %s. alt = %d, delta heading = %d", _unitName, _playername, tostring(unitinzone), unitalt, deltaheading)
local text=string.format("Checking still in zone. Unit = %s, player = %s in zone = %s. alt = %d, delta heading = %d", _unitName, _playername, tostring(unitinzone), unitalt, deltaheading)
self:T2(RANGE.id..text)
-- Check if player is in strafe zone and below max alt.
@ -1895,11 +1984,32 @@ function RANGE:_AddF10Commands(_unitName)
-- Enable switch so we don't do this twice.
self.MenuAddedTo[_gid] = true
-- Main F10 menu: F10/On the Range/<Range Name>/
if RANGE.MenuF10[_gid] == nil then
RANGE.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "On the Range")
-- Range root menu path.
local _rangePath=nil
if RANGE.MenuF10Root then
-------------------
-- MISSION LEVEL --
-------------------
_rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10Root)
else
-----------------
-- GROUP LEVEL --
-----------------
-- Main F10 menu: F10/On the Range/<Range Name>/
if RANGE.MenuF10[_gid] == nil then
RANGE.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "On the Range")
end
_rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10[_gid])
end
local _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10[_gid])
local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Statistics", _rangePath)
local _markPath = missionCommands.addSubMenuForGroup(_gid, "Mark Targets", _rangePath)
local _settingsPath = missionCommands.addSubMenuForGroup(_gid, "My Settings", _rangePath)
@ -1932,9 +2042,11 @@ function RANGE:_AddF10Commands(_unitName)
missionCommands.addCommandForGroup(_gid, "White Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.White)
missionCommands.addCommandForGroup(_gid, "Yellow Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Yellow)
-- F10/On the Range/<Range Name>/My Settings/
missionCommands.addCommandForGroup(_gid, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName)
missionCommands.addCommandForGroup(_gid, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName)
missionCommands.addCommandForGroup(_gid, "Smoke Impact On/Off", _settingsPath, self._SmokeBombImpactOnOff, self, _unitName)
missionCommands.addCommandForGroup(_gid, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName)
missionCommands.addCommandForGroup(_gid, "All Messages On/Off", _settingsPath, self._MessagesToPlayerOnOff, self, _unitName)
-- F10/On the Range/<Range Name>/Range Information
missionCommands.addCommandForGroup(_gid, "General Info", _infoPath, self._DisplayRangeInfo, self, _unitName)
missionCommands.addCommandForGroup(_gid, "Weather Report", _infoPath, self._DisplayRangeWeather, self, _unitName)
@ -2114,7 +2226,7 @@ function RANGE:_ResetRangeStats(_unitName)
self.strafePlayerResults[_playername] = nil
self.bombPlayerResults[_playername] = nil
local text=string.format("%s, %s, your range stats were cleared.", self.rangename, _playername)
self:DisplayMessageToGroup(_unit, text, 5)
self:DisplayMessageToGroup(_unit, text, 5, false, true)
end
end
@ -2124,33 +2236,35 @@ end
-- @param #string _text Message text.
-- @param #number _time Duration how long the message is displayed.
-- @param #boolean _clear Clear up old messages.
function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear)
-- @param #boolean display If true, display message regardless of player setting "Messages Off".
function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear, display)
self:F({unit=_unit, text=_text, time=_time, clear=_clear})
-- Defaults
_time=_time or self.Tmsg
if _clear==nil then
if _clear==nil or _clear==false then
_clear=false
else
_clear=true
end
-- Group ID.
local _gid=_unit:GetGroup():GetID()
if _gid and not self.examinerexclusive then
if _clear == true then
trigger.action.outTextForGroup(_gid, _text, _time, _clear)
else
trigger.action.outTextForGroup(_gid, _text, _time)
end
-- Get playername and player settings
local _, playername=self:_GetPlayerUnitAndName(_unit:GetName())
local playermessage=self.PlayerSettings[playername].messages
-- Send message to player if messages enabled and not only for the examiner.
if _gid and (playermessage==true or display) and (not self.examinerexclusive) then
trigger.action.outTextForGroup(_gid, _text, _time, _clear)
end
-- Send message to examiner.
if self.examinergroupname~=nil then
local _examinerid=GROUP:FindByName(self.examinergroupname):GetID()
if _examinerid then
if _clear == true then
trigger.action.outTextForGroup(_examinerid, _text, _time, _clear)
else
trigger.action.outTextForGroup(_examinerid, _text, _time)
end
trigger.action.outTextForGroup(_examinerid, _text, _time, _clear)
end
end
@ -2172,7 +2286,7 @@ function RANGE:_SmokeBombImpactOnOff(unitname)
self.PlayerSettigs[playername].smokebombimpact=true
text=string.format("%s, %s, smoking impact points of bombs is now ON.", self.rangename, playername)
end
self:_DisplayMessageToGroup(unit, text, 5)
self:_DisplayMessageToGroup(unit, text, 5, false, true)
end
end
@ -2193,7 +2307,27 @@ function RANGE:_SmokeBombDelayOnOff(unitname)
self.PlayerSettigs[playername].delaysmoke=true
text=string.format("%s, %s, delayed smoke of bombs is now ON.", self.rangename, playername)
end
self:_DisplayMessageToGroup(unit, text, 5)
self:_DisplayMessageToGroup(unit, text, 5, false, true)
end
end
--- Toggle display messages to player.
-- @param #RANGE self
-- @param #string unitname Name of the player unit.
function RANGE:_MessagesToPlayerOnOff(unitname)
self:F(unitname)
local unit, playername = self:_GetPlayerUnitAndName(unitname)
if unit and playername then
local text
if self.PlayerSettings[playername].messages==true then
text=string.format("%s, %s, display of ALL messages is now OFF.", self.rangename, playername)
else
text=string.format("%s, %s, display of ALL messages is now ON.", self.rangename, playername)
end
self:_DisplayMessageToGroup(unit, text, 5, false, true)
self.PlayerSettings[playername].messages=not self.PlayerSettings[playername].messages
end
end
@ -2214,7 +2348,7 @@ function RANGE:_FlareDirectHitsOnOff(unitname)
self.PlayerSettings[playername].flaredirecthits=true
text=string.format("%s, %s, flaring direct hits is now ON.", self.rangename, playername)
end
self:_DisplayMessageToGroup(unit, text, 5)
self:_DisplayMessageToGroup(unit, text, 5, false, true)
end
end
@ -2332,7 +2466,7 @@ function RANGE:_smokecolor2text(color)
elseif color==SMOKECOLOR.White then
txt="white"
else
txt=string.format("unkown color (%s)", tostring(color))
txt=string.format("unknown color (%s)", tostring(color))
end
return txt
@ -2355,7 +2489,7 @@ function RANGE:_flarecolor2text(color)
elseif color==FLARECOLOR.Yellow then
txt="yellow"
else
txt=string.format("unkown color (%s)", tostring(color))
txt=string.format("unknown color (%s)", tostring(color))
end
return txt

View File

@ -45,6 +45,11 @@
-- **PLEASE NOTE** that his class is work in progress and in an early **alpha** stage. Many/most things work already very nicely but there a lot of cases I did not run into yet.
-- Therefore, your *constructive* feedback is both necessary and appreciated!
--
-- ## Discussion
--
-- If you have questions or suggestions, visit the MOOSE Discord [#ops-airboss](https://discordapp.com/channels/378590350614462464/527363141185830915) channel.
-- There you also find an example mission and the necessary voice over sound files. Check the **pinned messages**.
--
-- ## IMPORTANT
--
-- Due to technical restrictions of DCS make sure you have:
@ -52,6 +57,12 @@
-- * Each player slot in a separate group. DCS does only allow to send messages to groups and not to individual units.
-- * Players are identified by their player name. Ensure that no two player have the same name, e.g. "New Callsign", as this will lead to unexpected results.
--
-- ## Youtube Videos
--
-- * [[MOOSE] Airboss - Groove Testing (WIP)](https://www.youtube.com/watch?v=94KHQxxX3UI)
-- * [[MOOSE] Airboss - Groove Test A-4E Community Mod](https://www.youtube.com/watch?v=ZbjD7FHiaHo)
--
--
-- ### Open Questions?
--
-- * Currently the script does not support spin patterns. Marshal releases flights only when there is a free slot in the landing pattern. How is this handled in real life?
@ -163,6 +174,8 @@
-- @field Core.Set#SET_GROUP squadsetAI AI groups in this set will be handled by the airboss.
-- @field #boolean menusingle If true, menu is optimized for a single carrier.
-- @field #number collisiondist Distance up to which collision checks are done.
-- @field #number Tmessage Default duration in seconds messages are displayed to players.
-- @field #string soundfolder Folder within the mission (miz) file where airboss sound files are located.
-- @extends Core.Fsm#FSM
--- Be the boss!
@ -246,9 +259,11 @@
-- The F10 radio menu can be used to post requests to Marshal but also provides information about the player and carrier status. Additionally, helper functions
-- can be called.
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMain.png)
--
-- By default, the script creates a submenu "Airboss" in the "F10 Other ..." menu and each @{#AIRBOSS} carrier gets its own submenu.
-- If you intend to have only one carrier, you can simplify the menu structure using the @{#AIRBOSS.SetMenuSingleCarrier} function, which will create all carrier specific menu entries directly
-- in the "Airboss" submenu. (Needless to say, that if you enable this and define mulitiple carriers, the menu structure will get completely screwed up.)
-- in the "Airboss" submenu. (Needless to say, that if you enable this and define multiple carriers, the menu structure will get completely screwed up.)
--
-- ## Root Menu
--
@ -274,9 +289,17 @@
-- ### Request Commence
--
-- This command can be used to request commencing from the marshal stack to the landing pattern. Necessary condition is that the player is in the lowest marshal stack
-- and that the number of aircraft in the landing pattern is smaller than four.
-- and that the number of aircraft in the landing pattern is smaller than four (or the number set by the mission designer).
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1Pattern.png)
--
-- The image displays the standard Case I Marshal pattern recovery. Pilots are supposed to fly a clockwise circle and descent between the **3** and **1** positions.
--
-- Commence should be performed at around the **3** position. If the pilot is in the lowest Marshal stack, and flies through this area, he is automatically cleared for the
-- landing pattern. In other words, there is no need for the "Request Commence" radio command. The zone can be marked via smoke or flared using the player's F10 radio menu.
--
-- A player can also request commencing if he is not registered in a marshal stack yet. If the pattern is free, Marshal will allow him to directly enter the landing pattern.
-- However, this is only possible when the Airboss has a nice day - see @{#AIRBOSS.SetAirbossNiceGuy}.
--
-- ### Request Refueling
--
@ -441,7 +464,7 @@
-- * **IM** In the Middle (0.5 NM = 926 m), middle one third of the glideslope.
-- * **IC** In Close (0.25 NM = 463 m), last one third of the glideslope.
-- * **AR** At the Ramp (0.027 NM = 50 m).
-- * **IW** In the Wiress (at the landing position).
-- * **IW** In the Wires (at the landing position).
--
-- Grading at each step includes the above calls, i.e.
--
@ -689,6 +712,27 @@
--
-- ===
--
-- # Sound Files
--
-- An important aspect of the AIRBOSS is that it uses voice overs for greater immersion. The necessary sound files can be obtained from the
-- MOOSE Discord in the [#ops-airboss](https://discordapp.com/channels/378590350614462464/527363141185830915) channel. Check out the **pinned messages**.
--
-- However, including sound files into a new mission is tedious as these usually need to be included into the mission **miz** file via (unused) triggers.
--
-- The default location inside the miz file is "l10n/DEFAULT/". But simply opening the *miz* file with e.g. [7-zip](https://www.7-zip.org/) and copying the files into that folder does not work.
-- The next time the mission is saved, files not included via trigger are automatically removed by DCS.
--
-- However, if you create a new folder inside the miz file, which contains the sounds, it will not be deleted and can be used. The location of the sound files can be specified
-- via the @{#AIRBOSS.SetSoundfilesFolder}(*folderpath*) function. The parameter *folderpath* defines the location of the sound files folder within the mission *miz* file.
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_SoundfilesFolder.png)
--
-- For example as
--
-- airbossStennis:SetSoundfilesFolder("Airboss Soundfiles/")
--
-- ===
--
-- # AI Handling
--
-- The @{#AIRBOSS} class allows to handle incoming AI units and integrate them into the marshal and landing pattern.
@ -855,6 +899,8 @@ AIRBOSS = {
squadsetAI = nil,
menusingle = nil,
collisiondist = nil,
Tmessage = nil,
soundfolder = nil,
}
--- Player aircraft types capable of landing on carriers.
@ -1520,13 +1566,17 @@ AIRBOSS.Difficulty={
-- @field #boolean subtitles If true, display subtitles of radio messages.
-- @extends #AIRBOSS.FlightGroup
--- Main radio menu: F10 Other/Airboss
--- Main group level radio menu: F10 Other/Airboss.
-- @field #table MenuF10
AIRBOSS.MenuF10={}
--- Airboss mission level F10 root menu.
-- @field #table MenuF10Root
AIRBOSS.MenuF10Root=nil
--- Airboss class version.
-- @field #string version
AIRBOSS.version="0.9.2"
AIRBOSS.version="0.9.3"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
@ -1707,6 +1757,7 @@ function AIRBOSS:New(carriername, alias)
-- Set update time intervals.
self:SetQueueUpdateTime()
self:SetStatusUpdateTime()
self:SetDefaultMessageDuration()
-- Menu options.
self:SetMenuMarkZones()
@ -2271,6 +2322,31 @@ function AIRBOSS:SetAirbossNiceGuy(switch)
return self
end
--- Set folder where the airboss sound files are located **within you mission (miz) file**.
-- The default path is "l10n/DEFAULT/" but sound files simply copied there will be removed by DCS the next time you save the mission.
-- However, if you create a new folder inside the miz file, which contains the sounds, it will not be deleted and can be used.
-- @param #AIRBOSS self
-- @param #string folderpath The path to the sound files, e.g. "Airboss Soundfiles/".
-- @return #AIRBOSS self
function AIRBOSS:SetSoundfilesFolder(folderpath)
-- Check that it ends with /
if folderpath then
local lastchar=string.sub(folderpath, -1)
if lastchar~="/" then
folderpath=folderpath.."/"
end
end
-- Folderpath.
self.soundfolder=folderpath
-- Info message.
self:I(self.lid..string.format("Setting sound files folder to: %s", self.soundfolder))
return self
end
--- Set time interval for updating player status and other things.
-- @param #AIRBOSS self
-- @param #number interval Time interval in seconds. Default 0.5 sec.
@ -2280,13 +2356,22 @@ function AIRBOSS:SetStatusUpdateTime(interval)
return self
end
--- Set duration how long messages are displayed to players.
-- @param #AIRBOSS self
-- @param #number duration Duration in seconds. Default 10 sec.
-- @return #AIRBOSS self
function AIRBOSS:SetDefaultMessageDuration(duration)
self.Tmessage=duration or 10
return self
end
--- Set Case I Marshal radius. This is the radius of the valid zone around "the post" aircraft are supposed to be holding in the Case I Marshal stack.
-- The post is 2.5 NM port of the carrier.
-- @param #AIRBOSS self
-- @param #number Radius in NM. Default 2.75 NM, which gives a diameter of 5.5 NM.
-- @param #number Radius in NM. Default 2.8 NM, which gives a diameter of 5.6 NM.
-- @return #AIRBOSS self
function AIRBOSS:SetMarshalRadius(radius)
self.marshalradius=UTILS.NMToMeters(radius or 2.75)
self.marshalradius=UTILS.NMToMeters(radius or 2.8)
return self
end
@ -3940,7 +4025,7 @@ function AIRBOSS:_ClearForLanding(flight)
local text=string.format("you are cleared for Case %d recovery.", flight.case)
-- Add a little delay because message that recovery window opened could come just before.
self:MessageToMarshal(text, "MARSHAL", flight.onboard, 10, false, 2)
self:MessageToMarshal(text, "MARSHAL", flight.onboard, nil, false, 2)
end
@ -4141,7 +4226,7 @@ function AIRBOSS:_WaitPlayer(playerData)
end
-- Send message.
self:MessageToMarshal(text, "AIRBOSS", playerData.onboard, 10)
self:MessageToMarshal(text, "AIRBOSS", playerData.onboard)
-- Add player flight to waiting queue.
table.insert(self.Qwaiting, playerData)
@ -6141,7 +6226,7 @@ function AIRBOSS:_Waiting(playerData)
-- Warning if player is inside the zone.
if inzone and Twaiting>3*60 and not playerData.warning then
local text=string.format("You are supposed to wait outside the 10 NM zone.")
self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 10)
self:MessageToPlayer(playerData, text, "AIRBOSS")
playerData.warning=true
end
@ -6373,7 +6458,8 @@ function AIRBOSS:_Commencing(playerData, zonecheck)
end
-- Message to player.
self:MessageToPlayer(playerData, text, "MARSHAL", nil, 3)
--self:MessageToPlayer(playerData, text, "MARSHAL", nil, 3)
self:MessageToPlayer(playerData, text, "MARSHAL")
end
-- Next step: depends on case recovery.
@ -7482,7 +7568,7 @@ function AIRBOSS:_CheckFoulDeck(playerData)
-- Player hint for flight students.
if playerData.difficulty~=AIRBOSS.Difficulty.HARD then
local text=string.format("overfly landing area and enter bolter pattern.")
self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 3)
self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3)
end
-- Set player parameters for foul deck
@ -7495,7 +7581,7 @@ function AIRBOSS:_CheckFoulDeck(playerData)
if foulunit then
local foulflight=self:_GetFlightFromGroupInQueue(foulunit:GetGroup(), self.flights)
if foulflight and not foulflight.ai then
self:MessageToPlayer(foulflight, "move your ass from my runway. NOW!", "AIRBOSS", nil, 10)
self:MessageToPlayer(foulflight, "move your ass from my runway. NOW!", "AIRBOSS")
end
end
end
@ -9134,7 +9220,7 @@ function AIRBOSS:_AbortPattern(playerData, X, Z, posData, patternwo)
self:T(self.lid..dtext)
-- Message to player.
self:MessageToPlayer(playerData, text, "LSO", nil, 20)
self:MessageToPlayer(playerData, text, "LSO")
if patternwo then
@ -9579,7 +9665,7 @@ function AIRBOSS:_Debrief(playerData)
-- Re-enter message.
local text=string.format("fly heading %03d° for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance))
self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 5)
self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 5)
else
@ -9607,7 +9693,7 @@ function AIRBOSS:_Debrief(playerData)
-- Airboss talkto!
local text=string.format("the deck was fouled but landed anyway. Airboss wants to talk to you!")
self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 3)
self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3)
end
@ -9630,7 +9716,7 @@ function AIRBOSS:_Debrief(playerData)
-- Airboss talkto!
local text=string.format("you were waved off but landed anyway. Airboss wants to talk to you!")
self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 3)
self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3)
end
@ -9663,7 +9749,7 @@ function AIRBOSS:_Debrief(playerData)
else
-- Message to player.
self:MessageToPlayer(playerData, "Undefined state after landing! Please report.", "ERROR", nil, 10)
self:MessageToPlayer(playerData, "Undefined state after landing! Please report.", "ERROR", nil, 20)
-- Next step.
playerData.step=AIRBOSS.PatternStep.UNDEFINED
@ -9739,7 +9825,7 @@ function AIRBOSS:_StepHint(playerData, step)
local text=string.format("Optimal setup at next step %s:%s", step, hint)
-- Send hint to player.
self:MessageToPlayer(playerData, text, "AIRBOSS", "", 10, false, 1)
self:MessageToPlayer(playerData, text, "AIRBOSS", "", nil, false, 1)
end
@ -10158,7 +10244,7 @@ function AIRBOSS:_CheckPatternUpdate()
-- 99, new final bearing XXX
local FB=self:GetFinalBearing(true)
local text=string.format("new final bearing %03d°.", FB)
self:MessageToMarshal(text, "AIRBOSS", "99", 10)
self:MessageToMarshal(text, "AIRBOSS", "99")
end
-- Reset parameters for next update check.
@ -10919,7 +11005,7 @@ function AIRBOSS:Sound2Player(playerData, radio, call, loud, delay)
-- Only to players with subtitle on or if noise is played.
if playerData.subtitles or self:_NeedsSubtitle(call) then
self:MessageToPlayer(playerData, subtitle, nil, "", call.subduration or 10, false, delay)
self:MessageToPlayer(playerData, subtitle, nil, "", call.subduration, false, delay)
end
end
@ -10985,7 +11071,9 @@ function AIRBOSS:_RadioFilename(call, loud)
-- Construct file name and subtitle.
local prefix=call.file or ""
local suffix=call.suffix or "ogg"
local path="l10n/DEFAULT/"
-- Path to sound files. Default is in the ME
local path=self.soundfolder or "l10n/DEFAULT/"
-- Loud version.
if loud then
@ -11014,7 +11102,7 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration
if playerData and message and message~="" then
-- Default duration.
duration=duration or 10
duration=duration or self.Tmessage
-- Format message.
local text
@ -11070,9 +11158,6 @@ end
-- @param #number delay Delay in seconds, before the message is displayed.
function AIRBOSS:MessageToPattern(message, sender, receiver, duration, clear, delay)
-- Local delay.
local _delay=delay or 0
-- Create new (fake) radio call to show the subtitile.
local call=self:_NewRadioCall(AIRBOSS.LSOCall.NOISE, sender or "LSO", message, duration, receiver, sender)
@ -11092,9 +11177,6 @@ end
-- @param #number delay Delay in seconds, before the message is displayed.
function AIRBOSS:MessageToMarshal(message, sender, receiver, duration, clear, delay)
-- Local delay.
local _delay=delay or 0
-- Create new (fake) radio call to show the subtitile.
local call=self:_NewRadioCall(AIRBOSS.MarshalCall.NOISE, sender or "MARSHAL", message, duration, receiver, sender)
@ -11123,7 +11205,7 @@ function AIRBOSS:_NewRadioCall(call, sender, subtitle, subduration, modexreceive
newcall.subtitle=subtitle or call.subtitle
-- Duration of subtitle display.
newcall.subduration=subduration or 10
newcall.subduration=subduration or self.Tmessage
-- Tail number of the receiver.
if self:_IsOnboard(modexreceiver) then
@ -11238,7 +11320,8 @@ function AIRBOSS:_Number2Sound(playerData, sender, number, delay)
local call=AIRBOSS[Sender][N] --#AIRBOSS.RadioCall
-- Create file name.
local filename=string.format("%s.%s", call.file, call.suffix)
--local filename=string.format("%s.%s", call.file, call.suffix)
local filename=self:_RadioFilename(call, false)
-- Play sound.
USERSOUND:New(filename):ToGroup(playerData.group, delay+wait)
@ -11332,17 +11415,40 @@ function AIRBOSS:_AddF10Commands(_unitName)
-- Enable switch so we don't do this twice.
self.menuadded[gid]=true
-- Main F10 menu: F10/Airboss/<Carrier Name>/
if AIRBOSS.MenuF10[gid]==nil then
AIRBOSS.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "Airboss")
end
-- Set menu root path.
local _rootPath=nil
if AIRBOSS.MenuF10Root then
------------------------
-- MISSON LEVEL MENUE --
------------------------
if self.menusingle then
-- F10/Airboss/...
_rootPath=AIRBOSS.MenuF10Root
else
-- F10/Airboss/<Carrier Alias>/...
_rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10Root)
end
-- F10/Airboss/<Carrier>
local _rootPath
if self.menusingle then
_rootPath=AIRBOSS.MenuF10[gid]
else
_rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid])
------------------------
-- GROUP LEVEL MENUES --
------------------------
-- Main F10 menu: F10/Airboss/
if AIRBOSS.MenuF10[gid]==nil then
AIRBOSS.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "Airboss")
end
if self.menusingle then
-- F10/Airboss/...
_rootPath=AIRBOSS.MenuF10[gid]
else
-- F10/Airboss/<Carrier Alias>/...
_rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid])
end
end
@ -12156,7 +12262,7 @@ function AIRBOSS:_DisplayQueue(_unitname, queue, qname)
end
-- Send message.
self:MessageToPlayer(playerData, text, nil, "", 10, true)
self:MessageToPlayer(playerData, text, nil, "", nil, true)
end
end
end
@ -12472,7 +12578,7 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName)
local fuel=playerData.unit:GetFuel()*100
local fuelstate=self:_GetFuelState(playerData.unit)
---
-- Number of units in group.
local _,nunitsGround=self:_GetFlightUnits(playerData, true)
local _,nunitsAirborne=self:_GetFlightUnits(playerData, false)
@ -12487,8 +12593,6 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName)
text=text..string.format("Skill Level: %s\n", playerData.difficulty)
text=text..string.format("Tail # %s (%s)\n", playerData.onboard, self:_GetACNickname(playerData.actype))
text=text..string.format("Fuel State: %.1f lbs/1000 (%.1f %%)\n", fuelstate/1000, fuel)
--text=text..string.format("Aircraft: %s\n", self:_GetACNickname(playerData.actype))
--text=text..string.format("Group: %s\n", playerData.group:GetName())
text=text..string.format("# units: %d (%d airborne)\n", nunitsGround, nunitsAirborne)
text=text..string.format("Section Lead: %s (%d/%d)", tostring(playerData.seclead), #playerData.section+1, self.NmaxSection+1)
for _,_sec in pairs(playerData.section) do

View File

@ -59,6 +59,7 @@
-- @field #boolean awacs If true, the groups gets the enroute task AWACS instead of tanker.
-- @field #number callsignname Number for the callsign name.
-- @field #number callsignnumber Number of the callsign name.
-- @field #string modex Tail number of the tanker.
-- @extends Core.Fsm#FSM
--- Recovery Tanker.
@ -292,6 +293,7 @@ RECOVERYTANKER = {
awacs = nil,
callsignname = nil,
callsignnumber = nil,
modex = nil,
}
--- Unique ID (global).
@ -300,7 +302,7 @@ RECOVERYTANKER.UID=0
--- Class version.
-- @field #string version
RECOVERYTANKER.version="1.0.5"
RECOVERYTANKER.version="1.0.6"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
@ -358,7 +360,7 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname)
self.alias=string.format("%s_%s_%02d", self.carrier:GetName(), self.tankergroupname, RECOVERYTANKER.UID)
-- Log ID.
self.lid=string.format("RECOVERYTANKER %s |", self.alias)
self.lid=string.format("RECOVERYTANKER %s | ", self.alias)
-- Init default parameters.
self:SetAltitude()
@ -617,6 +619,15 @@ function RECOVERYTANKER:SetCallsign(callsignname, callsignnumber)
return self
end
--- Set modex (tail number) of the tanker.
-- @param #RECOVERYTANKER self
-- @param #number modex Tail number.
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetModex(modex)
self.modex=modex
return self
end
--- Set takeoff type.
-- @param #RECOVERYTANKER self
-- @param #number takeofftype Takeoff type.
@ -810,8 +821,10 @@ function RECOVERYTANKER:onafterStart(From, Event, To)
-- Handle events.
self:HandleEvent(EVENTS.EngineShutdown)
self:HandleEvent(EVENTS.Refueling, self._RefuelingStart) --Need explcit functions sice OnEventRefueling and OnEventRefuelingStop did not hook.
self:HandleEvent(EVENTS.Refueling, self._RefuelingStart) --Need explicit functions since OnEventRefueling and OnEventRefuelingStop did not hook!
self:HandleEvent(EVENTS.RefuelingStop, self._RefuelingStop)
self:HandleEvent(EVENTS.Crash, self._OnEventCrashOrDead)
self:HandleEvent(EVENTS.Dead, self._OnEventCrashOrDead)
-- Spawn tanker. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine.
local Spawn=SPAWN:NewWithAlias(self.tankergroupname, self.alias)
@ -820,6 +833,7 @@ function RECOVERYTANKER:onafterStart(From, Event, To)
Spawn:InitRadioCommsOnOff(true)
Spawn:InitRadioFrequency(self.RadioFreq)
Spawn:InitRadioModulation(self.RadioModu)
Spawn:InitModex(self.modex)
-- Spawn on carrier.
if self.takeoff==SPAWN.Takeoff.Air then
@ -935,6 +949,7 @@ function RECOVERYTANKER:onafterStatus(From, Event, To)
self.tanker:InitRadioCommsOnOff(true)
self.tanker:InitRadioFrequency(self.RadioFreq)
self.tanker:InitRadioModulation(self.RadioModu)
self.tanker:InitModex(self.modex)
-- Respawn tanker.
self.tanker=self.tanker:Respawn(nil, true)
@ -991,14 +1006,17 @@ function RECOVERYTANKER:onafterStatus(From, Event, To)
-- TANKER is DEAD --
--------------------
-- Stop FSM.
self:Stop()
if not self:IsStopped() then
-- Stop FSM.
self:Stop()
-- Restart FSM after 5 seconds.
if self.respawn then
self:__Start(5)
end
-- Restart FSM after 5 seconds.
if self.respawn then
self:__Start(5)
end
end
end
@ -1103,9 +1121,22 @@ end
-- @param #string Event Event.
-- @param #string To To state.
function RECOVERYTANKER:onafterStop(From, Event, To)
-- Unhandle events.
self:UnHandleEvent(EVENTS.EngineShutdown)
self:UnHandleEvent(EVENTS.Refueling)
self:UnHandleEvent(EVENTS.RefuelingStop)
self:UnHandleEvent(EVENTS.Dead)
self:UnHandleEvent(EVENTS.Crash)
-- If tanker is alive, despawn it.
if self.helo and self.helo:IsAlive() then
self:I(self.lid.."Stopping FSM and despawning tanker.")
self.tanker:Destroy()
else
self:I(self.lid.."Stopping FSM. Tanker was not alive.")
end
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@ -1139,6 +1170,7 @@ function RECOVERYTANKER:OnEventEngineShutdown(EventData)
group:InitRadioCommsOnOff(true)
group:InitRadioFrequency(self.RadioFreq)
group:InitRadioModulation(self.RadioModu)
group:InitModex(self.modex)
-- Respawn tanker.
-- Delaying respawn due to DCS bug https://github.com/FlightControl-Master/MOOSE/issues/1076
@ -1221,6 +1253,37 @@ function RECOVERYTANKER:_RefuelingStop(EventData)
end
--- A unit crashed or died.
-- @param #RECOVERYTANKER self
-- @param Core.Event#EVENTDATA EventData Event data.
function RECOVERYTANKER:_OnEventCrashOrDead(EventData)
self:F2({eventdata=EventData})
-- Check that there is an initiating unit in the event data.
if EventData and EventData.IniUnit then
-- Crashed or dead unit.
local unit=EventData.IniUnit
local unitname=tostring(EventData.IniUnitName)
-- Check that it was the tanker that crashed.
if EventData.IniGroupName==self.tanker:GetName() then
-- Error message.
self:E(self.lid..string.format("Recovery tanker %s crashed!", unitname))
-- Stop FSM.
self:Stop()
-- Restart.
if self.respawn then
self:__Start(5)
end
end
end
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- MISC functions

View File

@ -11,6 +11,12 @@
-- * Multiple helos at different carriers due to object oriented approach.
-- * Finite State Machine (FSM) implementation.
--
-- ## Known (DCS) Issues
--
-- * CH-53E does only report 27.5% fuel even if fuel is set to 100% in the ME. See [bug report](https://forums.eagle.ru/showthread.php?t=223712)
-- * CH-53E does not accept USS Tarawa as landing airbase (even it can be spawned on it).
-- * Helos dont move away from their landing position on carriers.
--
-- ===
--
-- ### Author: **funkyfranky**
@ -50,6 +56,7 @@
-- @field #number hid Unit ID of the helo group. (Global) Running number.
-- @field #string alias Alias of the spawn group.
-- @field #number uid Unique ID of this helo.
-- @field #number modex Tail number of the helo.
-- @extends Core.Fsm#FSM
--- Rescue Helo
@ -219,6 +226,7 @@ RESCUEHELO = {
carrierstop = nil,
alias = nil,
uid = 0,
modex = nil,
}
--- Unique ID (global).
@ -227,7 +235,7 @@ RESCUEHELO.UID=0
--- Class version.
-- @field #string version
RESCUEHELO.version="1.0.3"
RESCUEHELO.version="1.0.5"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
@ -281,7 +289,7 @@ function RESCUEHELO:New(carrierunit, helogroupname)
self.alias=string.format("%s_%s_%02d", self.carrier:GetName(), self.helogroupname, RESCUEHELO.UID)
-- Log ID.
self.lid=string.format("RESCUEHELO %s |", self.alias)
self.lid=string.format("RESCUEHELO %s | ", self.alias)
-- Init defaults.
self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName()))
@ -317,12 +325,14 @@ function RESCUEHELO:New(carrierunit, helogroupname)
self:SetStartState("Stopped")
-- Add FSM transitions.
-- From State --> Event --> To State
-- From State --> Event --> To State
self:AddTransition("Stopped", "Start", "Running")
self:AddTransition("Running", "Rescue", "Rescuing")
self:AddTransition("Running", "RTB", "Returning")
self:AddTransition("Rescuing", "RTB", "Returning")
self:AddTransition("*", "Run", "Running")
self:AddTransition("Returning", "Returned", "Returned")
self:AddTransition("Running", "Run", "Running")
self:AddTransition("Returned", "Run", "Running")
self:AddTransition("*", "Status", "*")
self:AddTransition("*", "Stop", "Stopped")
@ -376,6 +386,25 @@ function RESCUEHELO:New(carrierunit, helogroupname)
-- @param #string To To state.
-- @param Wrapper.Airbase#AIRBASE airbase The airbase to return to. Default is the home base.
--- Triggers the FSM event "Returned" after the helo has landed.
-- @function [parent=#RESCUEHELO] Returned
-- @param #RESCUEHELO self
-- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed.
--- Triggers the delayed FSM event "Returned" after the helo has landed.
-- @function [parent=#RESCUEHELO] __Returned
-- @param #RESCUEHELO self
-- @param #number delay Delay in seconds.
-- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed.
--- On after "Returned" event user function. Called when a the the helo has landed at an airbase.
-- @function [parent=#RESCUEHELO] OnAfterReturned
-- @param #RESCUEHELO self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed.
--- Triggers the FSM event "Run".
-- @function [parent=#RESCUEHELO] Run
@ -599,6 +628,15 @@ function RESCUEHELO:SetRespawnInAir()
return self
end
--- Set modex (tail number) of the helo.
-- @param #RESCUEHELO self
-- @param #number modex Tail number.
-- @return #RESCUEHELO self
function RESCUEHELO:SetModex(modex)
self.modex=modex
return self
end
--- Use an uncontrolled aircraft already present in the mission rather than spawning a new helo as initial rescue helo.
-- This can be useful when interfaced with, e.g., a warehouse.
-- The group name is the one specified in the @{#RESCUEHELO.New} function.
@ -694,44 +732,24 @@ function RESCUEHELO:OnEventLand(EventData)
MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug)
self:T(self.lid..text)
-- Helo has rescued someone.
-- TODO: Add "Rescued" event.
if self:IsRescuing() then
self:T(self.lid..string.format("Rescue helo %s returned from rescue operation.", groupname))
end
-- Check if takeoff air or respawn in air is set. Landing event should not happen unless the helo was on a rescue mission.
if self.takeoff==SPAWN.Takeoff.Air or self.respawninair then
if self:IsRescuing() then
if not self:IsRescuing() then
self:T(self.lid..string.format("Rescue helo %s returned from rescue operation.", groupname))
-- Respawn helo at current airbase.
SCHEDULER:New(nil, group.RespawnAtCurrentAirbase, {group}, 3)
else
self:T2(self.lid..string.format("WARNING: Rescue helo %s landed. This should not happen for Takeoff=Air or respawninair=true unless a rescue operation finished.", groupname))
-- Respawn helo at current airbase anyway.
if self.respawn then
SCHEDULER:New(nil, group.RespawnAtCurrentAirbase, {group}, 3)
end
self:E(self.lid..string.format("WARNING: Rescue helo %s landed. This should not happen for Takeoff=Air or respawninair=true and no rescue operation in progress.", groupname))
end
else
-- Respawn helo at current airbase.
if self.respawn then
SCHEDULER:New(nil, group.RespawnAtCurrentAirbase, {group}, 3)
end
end
-- Restart the formation.
self:__Run(10)
-- Trigger returned event. Respawn at current airbase.
self:__Returned(3, EventData.Place)
end
end
@ -789,6 +807,11 @@ function RESCUEHELO:_OnEventCrashOrEject(EventData)
-- Stop FSM.
self:Stop()
-- Restart.
if self.respawn then
self:__Start(5)
end
end
end
@ -813,7 +836,7 @@ function RESCUEHELO:onafterStart(From, Event, To)
-- Handle events.
--self:HandleEvent(EVENTS.Birth)
self:HandleEvent(EVENTS.Land)
self:HandleEvent(EVENTS.Crash, self._OnEventCrashOrEject)
self:HandleEvent(EVENTS.Crash, self._OnEventCrashOrEject)
self:HandleEvent(EVENTS.Ejection, self._OnEventCrashOrEject)
-- Delay before formation is started.
@ -822,6 +845,9 @@ function RESCUEHELO:onafterStart(From, Event, To)
-- Spawn helo. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine.
local Spawn=SPAWN:NewWithAlias(self.helogroupname, self.alias)
-- Set modex for spawn.
Spawn:InitModex(self.modex)
-- Spawn in air or at airbase.
if self.takeoff==SPAWN.Takeoff.Air then
@ -920,11 +946,14 @@ function RESCUEHELO:onafterStatus(From, Event, To)
-- HELO is ALIVE --
-------------------
-- Get relative fuel wrt to initial fuel of helo (DCS bug https://forums.eagle.ru/showthread.php?t=223712)
local fuel=self.helo:GetFuel()/self.HeloFuel0*100
-- Get (relative) fuel wrt to initial fuel of helo (DCS bug https://forums.eagle.ru/showthread.php?t=223712)
local fuel=self.helo:GetFuel()*100
local fuelrel=fuel/self.HeloFuel0
local life=self.helo:GetUnit(1):GetLife()
local life0=self.helo:GetUnit(1):GetLife0()
-- Report current fuel.
local text=string.format("Rescue Helo %s: state=%s fuel=%.1f", self.helo:GetName(), self:GetState(), fuel)
local text=string.format("Rescue Helo %s: state=%s fuel=%.1f, rel.fuel=%.1f, life=%.1f/%.1f", self.helo:GetName(), self:GetState(), fuel, fuelrel, life, life0)
MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug)
self:T(self.lid..text)
@ -939,9 +968,16 @@ function RESCUEHELO:onafterStatus(From, Event, To)
-- Check if respawn is enabled.
if self.respawn then
-- Set modex for respawn.
self.helo:InitModex(self.modex)
-- Respawn helo in air.
self.helo=self.helo:Respawn(nil, true)
-- XXX: ATTENTION: if helo automatically RTBs on low fuel, it goes a bit cazy. The formation is not stopped and he partially dives into the water.
-- Also trying to find a ship to land on he flies right through it.
--self.helo:OptionRTBBingoFuel(false)
end
else
@ -955,15 +991,7 @@ function RESCUEHELO:onafterStatus(From, Event, To)
elseif self:IsRescuing() then
if self.rtb then
-- Send helo back to base.
--self:RTB()
-- Switch to false.
self.rtb=false
end
-- Helo is on a rescue mission.
end
@ -972,25 +1000,28 @@ function RESCUEHELO:onafterStatus(From, Event, To)
self:__Status(-30)
end
else
------------------
-- HELO is DEAD --
------------------
-- Stop FSM.
self:Stop()
if not self:IsStopped() then
-- Stop FSM.
self:Stop()
-- Restart FSM after 5 seconds.
if self.respawn then
self:__Start(5)
end
-- Restart FSM after 5 seconds.
if self.respawn then
self:__Start(5)
end
end
end
--- On after "Run" event. FSM will go to "Running" state. If formation is topped, it will be started again.
--- On after "Run" event. FSM will go to "Running" state. If formation is stopped, it will be started again.
-- @param #RESCUEHELO self
-- @param #string From From state.
-- @param #string Event Event.
@ -1037,7 +1068,7 @@ function RESCUEHELO:_TaskRTB()
-- Task script.
local DCSScript = {}
DCSScript[#DCSScript+1] = string.format('local mycarrier = UNIT:FindByName(\"%s\") ', carriername) -- The carrier unit that holds the self object.
DCSScript[#DCSScript+1] = string.format('local myhelo = mycarrier:GetState(mycarrier, \"RESCUEHELO_%d\") ', self.uid) -- Get the RECOVERYTANKER self object.
DCSScript[#DCSScript+1] = string.format('local myhelo = mycarrier:GetState(mycarrier, \"RESCUEHELO_%d\") ', self.uid) -- Get the RESCUEHELO self object.
DCSScript[#DCSScript+1] = string.format('myhelo:RTB()') -- Call the function, e.g. myhelo.(self)
-- Create task.
@ -1155,16 +1186,58 @@ function RESCUEHELO:onafterRTB(From, Event, To, airbase)
self:RouteRTB(airbase)
end
--- On after Stop event. Unhandle events and stop status updates.
--- On after Returned event. Helo has landed.
-- @param #RESCUEHELO self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Airbase#AIRBASE airbase The base to which the helo has returned.
function RESCUEHELO:onafterReturned(From, Event, To, airbase)
if airbase then
local airbasename=airbase:GetName()
self:T(self.lid..string.format("Helo returned to airbase %s", tostring(airbasename)))
else
self:E(self.lid..string.format("WARNING: Helo landed but airbase (EventData.Place) is nil!"))
end
-- Respawn helo at current airbase.
if self.respawn then
-- Set modex for respawn.
self.helo:InitModex(self.modex)
-- Respawn helo at current airbase.
self.helo:RespawnAtCurrentAirbase()
-- Restart the formation.
self:__Run(10)
end
end
--- On after Stop event. Unhandle events and stop status updates. If helo is alive, it is despawned.
-- @param #RESCUEHELO self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function RESCUEHELO:onafterStop(From, Event, To)
-- Stop formation
self.formation:Stop()
-- Unhandle events.
self:UnHandleEvent(EVENTS.Land)
self:UnHandleEvent(EVENTS.Crash)
self:UnHandleEvent(EVENTS.Ejection)
-- If helo is alive, despawn it.
if self.helo and self.helo:IsAlive() then
self:I(self.lid.."Stopping FSM and despawning helo.")
self.helo:Destroy()
else
self:I(self.lid.."Stopping FSM. Helo was not alive.")
end
end
@ -1196,6 +1269,9 @@ function RESCUEHELO:RouteRTB(RTBAirbase, Speed)
-- Set route points.
Template.route.points=Points
-- Set modex for respawn.
self.helo:InitModex(self.modex)
-- Respawn the group.
self.helo=self.helo:Respawn(Template, true)

View File

@ -890,4 +890,19 @@ function UTILS.GetMagneticDeclination(map)
return declination
end
--- Checks if a file exists or not. This requires **io** to be desanitized.
-- @param #string file File that should be checked.
-- @return #boolean True if the file exists, false if the file does not exist or nil if the io module is not available and the check could not be performed.
function UTILS.FileExists(file)
if io then
local f=io.open(file, "r")
if f~=nil then
io.close(f)
return true
else
return false
end
else
return nil
end
end

View File

@ -657,7 +657,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius,
if r1 and r2 then
local safedist=(r1+r2)*1.1
local safe = (dist > safedist)
self:E(string.format("r1=%.1f r2=%.1f s=%.1f d=%.1f ==> safe=%s", r1, r2, safedist, dist, tostring(safe)))
self:T2(string.format("r1=%.1f r2=%.1f s=%.1f d=%.1f ==> safe=%s", r1, r2, safedist, dist, tostring(safe)))
return safe
else
return true
@ -710,7 +710,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius,
if verysafe and (parkingspot.Free==false or parkingspot.TOAC==true) then
-- DCS getParking() routine returned that spot is not free.
self:E(string.format("%s: Parking spot id %d NOT free (or aircraft has not taken off yet). Free=%s, TOAC=%s.", airport, parkingspot.TerminalID, tostring(parkingspot.Free), tostring(parkingspot.TOAC)))
self:T(string.format("%s: Parking spot id %d NOT free (or aircraft has not taken off yet). Free=%s, TOAC=%s.", airport, parkingspot.TerminalID, tostring(parkingspot.Free), tostring(parkingspot.TOAC)))
else
@ -790,6 +790,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius,
table.insert(validspots, {Coordinate=_spot, TerminalID=_termid})
end
nvalid=nvalid+1
self:I(string.format("%s: Parking spot id %d free. Nfree=%d/%d.", airport, _termid, nvalid,_nspots))
end
end -- loop over units

View File

@ -400,7 +400,8 @@ function CONTROLLABLE:SetTask( DCSTask, WaitTime )
local Controller = self:_GetController()
--self:I( "Before SetTask" )
Controller:setTask( DCSTask )
self:I( { ControllableName = self:GetName(), DCSTask = DCSTask } )
-- AI_FORMATION class (used by RESCUEHELO) calls SetTask twice per second! hence spamming the DCS log file ==> setting this to trace.
self:T( { ControllableName = self:GetName(), DCSTask = DCSTask } )
else
BASE:E( { DCSControllableName .. " is not alive anymore.", DCSTask = DCSTask } )
end
@ -408,7 +409,8 @@ function CONTROLLABLE:SetTask( DCSTask, WaitTime )
if not WaitTime or WaitTime == 0 then
SetTask( self, DCSTask )
self:I( { ControllableName = self:GetName(), DCSTask = DCSTask } )
-- See above.
self:T( { ControllableName = self:GetName(), DCSTask = DCSTask } )
else
self.TaskScheduler:Schedule( self, SetTask, { DCSTask }, WaitTime )
end

View File

@ -1457,11 +1457,16 @@ end
--- Sets the radio comms on or off when the group is respawned. Same as checking/unchecking the COMM box in the mission editor.
-- @param #GROUP self
-- @param #number switch If true (or nil), enables the radio comms. If false, disables the radio for the spawned group.
-- @param #boolean switch If true (or nil), enables the radio comms. If false, disables the radio for the spawned group.
-- @return #GROUP self
function GROUP:InitRadioCommsOnOff(switch)
self:F({switch=switch} )
self.InitRespawnRadio=switch or true
self:F({switch=switch})
if switch==true or switch==nil then
self.InitRespawnRadio=true
else
self.InitRespawnRadio=false
end
return self
end
--- Sets the radio frequency of the group when it is respawned.
@ -1469,7 +1474,7 @@ end
-- @param #number frequency The frequency in MHz.
-- @return #GROUP self
function GROUP:InitRadioFrequency(frequency)
self:F({frequency=frequency} )
self:F({frequency=frequency})
self.InitRespawnFreq=frequency
@ -1490,6 +1495,17 @@ function GROUP:InitRadioModulation(modulation)
return self
end
--- Sets the modex (tail number) of the first unit of the group. If more units are in the group, the number is increased with every unit.
-- @param #GROUP self
-- @param #string modex Tail number of the first unit.
-- @return #GROUP self
function GROUP:InitModex(modex)
self:F({modex=modex})
if modex then
self.InitRespawnModex=tonumber(modex)
end
return self
end
--- Respawn the @{Wrapper.Group} at a @{Point}.
-- The method will setup the new group template according the Init(Respawn) settings provided for the group.
@ -1641,6 +1657,13 @@ function GROUP:Respawn( Template, Reset )
end
-- Set tail number.
if self.InitRespawnModex then
for UnitID=1,#Template.units do
Template.units[UnitID].onboard_num=string.format("%03d", self.InitRespawnModex+(UnitID-1))
end
end
-- Set radio frequency and modulation.
if self.InitRespawnRadio then
Template.communication=self.InitRespawnRadio