diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua index 018d18839..91a6eaffa 100644 --- a/Moose Development/Moose/AI/AI_Air.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -7,7 +7,7 @@ -- === -- -- @module AI.AI_Air --- @image AI_Air_Operations.JPG +-- @image MOOSE.JPG --- @type AI_AIR -- @extends Core.Fsm#FSM_CONTROLLABLE diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index d69ae0097..d65aae7a2 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -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 @@ -1218,7 +1234,13 @@ function SPAWN:SpawnWithIndex( SpawnIndex ) SpawnTemplate.units[UnitID].skill = self.SpawnInitSkill 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 diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 394f134f2..a3e73b21a 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -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. @@ -310,6 +318,9 @@ function RANGE:New(rangename) local text=string.format("RANGE script version %s - creating new RANGE object of name: %s.", RANGE.version, self.rangename) self:E(RANGE.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) + + -- Defaults + self:SetDefaultPlayerSmokeBomb() -- Return object. return self @@ -317,93 +328,102 @@ 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 strafing targets. - _count=0 - for _,_target in pairs(self.strafeTargets) do - _count=_count+1 - - for _,_unit in pairs(_target.targets) do + -- Count bomb targets. + local _count=0 + for _,_target in pairs(self.bombingTargets) do + _count=_count+1 + + -- Get range location. if _location==nil then - _location=_unit:GetCoordinate() + _location=_target.target:GetCoordinate() --Core.Point#COORDINATE end end - end - self.nstrafetargets=_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 - - 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 - - -- 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 - - -- 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) - - -- 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 - - -- 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()) + self.nbombtargets=_count - 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") + -- Count strafing targets. + _count=0 + for _,_target in pairs(self.strafeTargets) do + _count=_count+1 + + for _,_unit in pairs(_target.targets) do + if _location==nil then + _location=_unit:GetCoordinate() + end + end + end + self.nstrafetargets=_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 + + 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 + + -- 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 + + -- 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) + + -- 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 + + -- 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. @@ -1894,12 +1983,33 @@ function RANGE:_AddF10Commands(_unitName) -- Enable switch so we don't do this twice. self.MenuAddedTo[_gid] = true - - -- Main F10 menu: F10/On the Range// - if RANGE.MenuF10[_gid] == nil then - RANGE.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "On the Range") - end - local _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10[_gid]) + + -- 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// + 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 _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//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, "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 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 diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 4fe125986..79b5a9970 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -45,12 +45,23 @@ -- **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: -- -- * 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? -- @@ -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 @@ -2696,7 +2781,7 @@ function AIRBOSS:onafterStart(From, Event, To) self:_CheckRecoveryTimes() -- Time stamp for checking queues. We substract 60 seconds so the routine is called right after status is called the first time. - self.Tqueue=timer.getTime()-60 + self.Tqueue=timer.getTime()-60 -- Handle events. self:HandleEvent(EVENTS.Birth) @@ -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,8 +11071,10 @@ 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 prefix=prefix.."_Loud" @@ -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) @@ -11331,20 +11414,43 @@ function AIRBOSS:_AddF10Commands(_unitName) -- Enable switch so we don't do this twice. self.menuadded[gid]=true - - -- Main F10 menu: F10/Airboss// - if AIRBOSS.MenuF10[gid]==nil then - AIRBOSS.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "Airboss") + + -- 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//... + _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10Root) + end + + else + ------------------------ + -- 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//... + _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid]) + end + end - -- F10/Airboss/ - local _rootPath - if self.menusingle then - _rootPath=AIRBOSS.MenuF10[gid] - else - _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid]) - end - -------------------------------- -- F10/Airboss//F1 Help @@ -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 diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 42f690bc8..d0fa243f9 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -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) @@ -990,15 +1005,18 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) -------------------- -- TANKER is DEAD -- -------------------- + + if not self:IsStopped() then - -- Stop FSM. - self:Stop() + -- Stop FSM. + self:Stop() - -- Restart FSM after 5 seconds. - if self.respawn then - self:__Start(5) + -- Restart FSM after 5 seconds. + if self.respawn then + self:__Start(5) + end + 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 diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 6f38337ca..cabe8390f 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -10,6 +10,12 @@ -- * Automatic rescuing of crashed or ejected pilots in the vicinity of the carrier. -- * 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. -- -- === -- @@ -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,45 +732,25 @@ function RESCUEHELO:OnEventLand(EventData) MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) self:T(self.lid..text) - if self:IsRescuing() then - + -- 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 - - 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)) + if not self:IsRescuing() then - -- 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 end - -- Restart the formation. - self:__Run(10) - + -- Trigger returned event. Respawn at current airbase. + self:__Returned(3, EventData.Place) + end end end @@ -787,7 +805,12 @@ function RESCUEHELO:_OnEventCrashOrEject(EventData) self:E(self.lid..string.format("Rescue helo %s crashed!", unitname)) -- Stop FSM. - self:Stop() + self:Stop() + + -- Restart. + if self.respawn then + self:__Start(5) + 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 @@ -865,7 +891,7 @@ function RESCUEHELO:onafterStart(From, Event, To) return end - else + else -- Spawn at airbase. self.helo=Spawn:SpawnAtAirbase(self.airbase, self.takeoff) @@ -920,13 +946,16 @@ 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) + self:T(self.lid..text) if self:IsRunning() then @@ -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 - -- Restart FSM after 5 seconds. - if self.respawn then - self:__Start(5) + -- Stop FSM. + self:Stop() + + -- Restart FSM after 5 seconds. + if self.respawn then + self:__Start(5) + end + end + 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) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 746d1c7c4..0ab538ef4 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -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 diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 717d4afcf..953ba2457 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -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 @@ -785,11 +785,12 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, if occupied then self:T(string.format("%s: Parking spot id %d occupied.", airport, _termid)) else - self:I(string.format("%s: Parking spot id %d free.", airport, _termid)) + self:I(string.format("%s: Parking spot id %d free.", airport, _termid)) if nvalid<_nspots then 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 diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 4b59ef8a2..f50040282 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -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 diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 72da87020..8525f0e83 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -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