From 6c35892f05188f5471d220d606e45cc50cdcfeae Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 1 May 2019 17:56:25 +0200 Subject: [PATCH 1/3] RANGE 2.0 Range v2.0.0 - Turned into FSM. - Added radial to bomb impact info. - Added option to add coordinates as bomb target. - Added option to save bombing results to file. AIRBOSS v1.0.0 - Added option to disable welcome message. - Increased output load/save. --- Moose Development/Moose/Functional/Range.lua | 919 ++++++++++++++----- Moose Development/Moose/Ops/Airboss.lua | 41 +- 2 files changed, 715 insertions(+), 245 deletions(-) diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 62e39cadf..09c6be614 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -43,6 +43,7 @@ -- @type RANGE -- @field #string ClassName Name of the Class. -- @field #boolean Debug If true, debug info is send as messages on the screen. +-- @field #string id String id of range for output in DCS log. -- @field #string rangename Name of the range. -- @field Core.Point#COORDINATE location Coordinate of the range location. -- @field #number rangeradius Radius of range defining its total size for e.g. smoking bomb impact points and sending radio messages. Default 5 km. @@ -76,6 +77,7 @@ -- @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. +-- @field #boolean autosafe If true, automatically save results every X seconds. -- @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. @@ -212,6 +214,7 @@ RANGE={ ClassName = "RANGE", Debug=false, + id=nil, rangename=nil, location=nil, rangeradius=5000, @@ -245,6 +248,7 @@ RANGE={ trackrockets=true, trackmissiles=true, defaultsmokebomb=true, + autosave=false, } --- Default range parameters. @@ -264,13 +268,59 @@ RANGE.Defaults={ foulline=610, } ---- Bomb target. self.bombingTargets, {name=name, target=unit, goodhitrange=goodhitrange, move=randommove, speed=speed} +--- Target type, i.e. unit, static, or coordinate. +-- @type RANGE.TargetType +-- @field #string UNIT Target is a unit. +-- @field #string STATIC Target is a static. +-- @field #string COORD Target is a coordinate. +RANGE.TargetType={ + UNIT="Unit", + STATIC="Static", + COORD="Coordinate", +} + +--- Player settings. +-- @type RANGE.PlayerData +-- @field #boolean smokebombimpact Smoke bomb impact points. +-- @field #boolean flaredirecthits Flare when player directly hits a target. +-- @field #number smokecolor Color of smoke. +-- @field #number flarecolor Color of flares. +-- @field #boolean messages Display info messages. +-- @field #string unitname Name of player aircraft unit. +-- @field #string playername Name of player. +-- @field #string airframe Aircraft type name. + +--- Bomb target data. -- @type RANGE.BombTarget --- @field #string name Name of unit? +-- @field #string name Name of unit. -- @field Wrapper.Unit#UNIT target Target unit. +-- @field Core.Point#COORDINATE coordinate Coordinate of the target. -- @field #number goodhitrange Range in meters for a good hit. -- @field #boolean move If true, unit move randomly. -- @field #number speed Speed of unit. +-- @field #RANGE.TargetType type Type of target. + +--- Strafe target data. +-- @type RANGE.StrafeTarget +-- @field #string name Name of the unit. +-- @field Core.Zone#ZONE_POLYGON polygon Polygon zone. +-- @field Core.Point#COORDINATE coordinate Center coordinate of the pit. +-- @field #number goodPass Number of hits for a good pass. +-- @field #table targets Table of target units. +-- @field #number foulline Foul line +-- @field #number smokepoints Number of smoke points. +-- @field #number heading Heading of pit. + +--- Bomb target result. +-- @type RANGE.BombResult +-- @field #string name Name of closest target. +-- @field #number distance Distance in meters. +-- @field #number radial Radial in degrees. +-- @field #string weapon Name of the weapon. +-- @field #string quality Hit quality. +-- @field #string player Player name. +-- @field #string airframe Aircraft type of player. +-- @field #number time Time via timer.getAbsTime() in seconds of impact. --- Global list of all defined range names. -- @field #table Names @@ -284,13 +334,9 @@ RANGE.MenuF10={} -- @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.3.0" +RANGE.version="2.0.0" --TODO list: --TODO: Verbosity level for messages. @@ -316,19 +362,86 @@ function RANGE:New(rangename) BASE:F({rangename=rangename}) -- Inherit BASE. - local self=BASE:Inherit(self, BASE:New()) -- #RANGE + local self=BASE:Inherit(self, FSM:New()) -- #RANGE -- Get range name. --TODO: make sure that the range name is not given twice. This would lead to problems in the F10 radio menu. self.rangename=rangename or "Practice Range" + -- Log id. + self.id=string.format("RANGE %s | ", self.rangename) + -- Debug info. - local text=string.format("RANGE script version %s - creating new RANGE object of name: %s.", RANGE.version, self.rangename) - self:E(RANGE.id..text) + local text=string.format("Script version %s - creating new RANGE object %s.", RANGE.version, self.rangename) + self:I(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) -- Defaults self:SetDefaultPlayerSmokeBomb() + + -- Start State. + self:SetStartState("Stopped") + + --- + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start RANGE script. + self:AddTransition("*", "Status", "*") -- Status of RANGE script. + self:AddTransition("*", "Impact", "*") -- Impact of bomb/rocket/missile. + self:AddTransition("*", "Save", "*") -- Save player results. + self:AddTransition("*", "Load", "*") -- Load player results. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the RANGE. Initializes parameters and starts event handlers. + -- @function [parent=#RANGE] Start + -- @param #RANGE self + + --- Triggers the FSM event "Start" after a delay. Starts the RANGE. Initializes parameters and starts event handlers. + -- @function [parent=#RANGE] __Start + -- @param #RANGE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the RANGE and all its event handlers. + -- @param #RANGE self + + --- Triggers the FSM event "Stop" after a delay. Stops the RANGE and all its event handlers. + -- @function [parent=#RANGE] __Stop + -- @param #RANGE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#RANGE] Status + -- @param #RANGE self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#RANGE] __Status + -- @param #RANGE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Impact". + -- @function [parent=#RANGE] Impact + -- @param #RANGE self + -- @param #RANGE.BombResult result Data of bombing run. + -- @param #RANGE.Playerdata player Data of player settings etc. + + --- Triggers the FSM delayed event "Impact". + -- @function [parent=#RANGE] __Impact + -- @param #RANGE self + -- @param #number delay Delay in seconds before the function is called. + -- @param #RANGE.BombResult result Data of the bombing run. + -- @param #RANGE.Playerdata player Data of player settings etc. + + --- On after "Impact" event user function. Called when a bomb/rocket/missile impacted. + -- @function [parent=#RANGE] OnAfterImpact + -- @param #RANGE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #RANGE.BombResult result Data of the bombing run. + -- @param #RANGE.Playerdata player Data of player settings etc. -- Return object. return self @@ -336,106 +449,102 @@ end --- Initializes number of targets and location of the range. Starts the event handlers. -- @param #RANGE self --- @param #number delay Delay in seconds, before the RANGE is started. Default immediately. --- @return self -function RANGE:Start(delay) - self:F() +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onafterStart() - if delay and delay>0 then - SCHEDULER:New(nil, self.Start, {self}, delay) - else + -- Location/coordinate of range. + local _location=nil - -- Location/coordinate of range. - local _location=nil + -- Count bomb targets. + local _count=0 + for _,_target in pairs(self.bombingTargets) do + _count=_count+1 - -- Count bomb targets. - local _count=0 - for _,_target in pairs(self.bombingTargets) do - _count=_count+1 - - -- Get range location. + -- Get range location. + if _location==nil then + _location=self:_GetBombTargetCoordinate(_target) + 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 if _location==nil then - _location=_target.target:GetCoordinate() --Core.Point#COORDINATE + _location=_unit:GetCoordinate() 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 - 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 + end + self.nstrafetargets=_count - -- 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 + -- 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(self.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(self.id..text) + MESSAGE:New(text,10):ToAllIf(self.Debug) + + -- Event handling. + if self.eventmoose then + -- Events are handled my MOOSE. + self:T(self.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(self.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()) + local _static=_target.type==RANGE.TargetType.STATIC - -- 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) + 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 - return self + -- 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 + + self:__Status(-60) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Set maximal strafing altitude. Player entering a strafe pit above that altitude are not registered for a valid pass. -- @param #RANGE self @@ -464,6 +573,22 @@ function RANGE:SetMessageTimeDuration(time) return self end +--- Automatically safe player results to disc. +-- @param #RANGE self +-- @return #RANGE self +function RANGE:SetAutosafeOn() + self.autosafe=true + return self +end + +--- Switch off auto safe player results. +-- @param #RANGE self +-- @return #RANGE self +function RANGE:SetAutosafeOff() + self.autosafe=false + 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. @@ -495,7 +620,7 @@ 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. +-- @param #boolean switch 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 @@ -511,7 +636,7 @@ end -- @param #number distance Threshold distance in km. Default 25 km. -- @return #RANGE self function RANGE:SetBombtrackThreshold(distance) - self.BombtrackThreshold=distance*1000 or 25*1000 + self.BombtrackThreshold=(distance or 25)*1000 return self end @@ -544,6 +669,15 @@ function RANGE:SetBombTargetSmokeColor(colorid) return self end +--- Set score bomb distance. +-- @param #RANGE self +-- @param #number distance Distance in meters. Default 1000 m. +-- @return #RANGE self +function RANGE:SetScoreBombDistance(distance) + self.scorebombdistance=distance or 1000 + 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. @@ -670,20 +804,20 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe if _isstatic==true then -- Add static object. - self:T(RANGE.id..string.format("Adding STATIC object %s as strafe target #%d.", _name, _i)) + self:T(self.id..string.format("Adding STATIC object %s as strafe target #%d.", _name, _i)) unit=STATIC:FindByName(_name, false) elseif _isstatic==false then -- Add unit object. - self:T(RANGE.id..string.format("Adding UNIT object %s as strafe target #%d.", _name, _i)) + self:T(self.id..string.format("Adding UNIT object %s as strafe target #%d.", _name, _i)) unit=UNIT:FindByName(_name) else -- Neither unit nor static object with this name could be found. local text=string.format("ERROR! Could not find ANY strafe target object with name %s.", _name) - self:E(RANGE.id..text) + self:E(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) end @@ -703,7 +837,7 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe -- Check if at least one target could be found. if ntargets==0 then local text=string.format("ERROR! No strafe target could be found when calling RANGE:AddStrafePit() for range %s", self.rangename) - self:E(RANGE.id..text) + self:E(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) return end @@ -757,13 +891,23 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe -- Create tires --_polygon:BoundZone() + + local st={} --#RANGE.StrafeTarget + st.name=_name + st.polygon=_polygon + st.coordinate=Ccenter + st.goodPass=goodpass + st.targets=_targets + st.foulline=foulline + st.smokepoints=p + st.heading=heading -- Add zone to table. - table.insert(self.strafeTargets, {name=_name, polygon=_polygon, coordinate= Ccenter, goodPass=goodpass, targets=_targets, foulline=foulline, smokepoints=p, heading=heading}) + table.insert(self.strafeTargets, st) -- Debug info 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) + self:T(self.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) return self @@ -835,14 +979,14 @@ function RANGE:AddBombingTargets(targetnames, goodhitrange, randommove) if _isstatic==true then local _static=STATIC:FindByName(name) - self:T2(RANGE.id..string.format("Adding static bombing target %s with hit range %d.", name, goodhitrange, false)) + self:T2(self.id..string.format("Adding static bombing target %s with hit range %d.", name, goodhitrange, false)) self:AddBombingTargetUnit(_static, goodhitrange) elseif _isstatic==false then local _unit=UNIT:FindByName(name) - self:T2(RANGE.id..string.format("Adding unit bombing target %s with hit range %d.", name, goodhitrange, randommove)) + self:T2(self.id..string.format("Adding unit bombing target %s with hit range %d.", name, goodhitrange, randommove)) self:AddBombingTargetUnit(_unit, goodhitrange) else - self:E(RANGE.id..string.format("ERROR! Could not find bombing target %s.", name)) + self:E(self.id..string.format("ERROR! Could not find bombing target %s.", name)) end end @@ -875,11 +1019,11 @@ function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) -- Debug or error output. if _isstatic==true then - self:T(RANGE.id..string.format("Adding STATIC bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) + self:T(self.id..string.format("Adding STATIC bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) elseif _isstatic==false then - self:T(RANGE.id..string.format("Adding UNIT bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) + self:T(self.id..string.format("Adding UNIT bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) else - self:E(RANGE.id..string.format("ERROR! No bombing target with name %s could be found. Carefully check all UNIT and STATIC names defined in the mission editor!", name)) + self:E(self.id..string.format("ERROR! No bombing target with name %s could be found. Carefully check all UNIT and STATIC names defined in the mission editor!", name)) end -- Get max speed of unit in km/h. @@ -888,9 +1032,46 @@ function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) speed=self:_GetSpeed(unit) end - -- Insert target to table. - table.insert(self.bombingTargets, {name=name, target=unit, goodhitrange=goodhitrange, move=randommove, speed=speed}) + local target={} --#RANGE.BombTarget + target.name=name + target.target=target + target.goodhitrange=goodhitrange + target.move=randommove + target.speed=speed + target.coordinate=unit:GetCoordinate() + if _isstatic then + target.type=RANGE.TargetType.STATIC + else + target.type=RANGE.TargetType.UNIT + end + -- Insert target to table. + table.insert(self.bombingTargets, target) + + return self +end + + +--- Add a coordinate of a bombing target. This +-- @param #RANGE self +-- @param Core.Point#COORDINATE coord The coordinate. +-- @param #string name Name of target. +-- @param #number goodhitrange Max distance from unit which is considered as a good hit. +-- @return #RANGE self +function RANGE:AddBombingTargetCoordinate(coord, name, goodhitrange) + + local target={} --#RANGE.BombTarget + target.name=name or "Bomb Target" + target.target=nil + target.goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange + target.move=false + target.speed=0 + target.coordinate=coord + target.type=RANGE.TargetType.COORD + + -- Insert target to table. + table.insert(self.bombingTargets, target) + return self end @@ -936,7 +1117,7 @@ function RANGE:GetFoullineDistance(namepit, namefoulline) elseif _staticpit==false then pit=UNIT:FindByName(namepit) else - self:E(RANGE.id..string.format("ERROR! Pit object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namepit)) + self:E(self.id..string.format("ERROR! Pit object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namepit)) end -- Get the unit or static foul line object. @@ -946,7 +1127,7 @@ function RANGE:GetFoullineDistance(namepit, namefoulline) elseif _staticfoul==false then foul=UNIT:FindByName(namefoulline) else - self:E(RANGE.id..string.format("ERROR! Foul line object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namefoulline)) + self:E(self.id..string.format("ERROR! Foul line object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namefoulline)) end -- Get the distance between the two objects. @@ -954,15 +1135,16 @@ function RANGE:GetFoullineDistance(namepit, namefoulline) if pit~=nil and foul~=nil then fouldist=pit:GetCoordinate():Get2DDistance(foul:GetCoordinate()) else - self:E(RANGE.id..string.format("ERROR! Foul line distance could not be determined. Check pit object name %s and foul line object name %s in the ME.", namepit, namefoulline)) + self:E(self.id..string.format("ERROR! Foul line distance could not be determined. Check pit object name %s and foul line object name %s in the ME.", namepit, namefoulline)) end - self:T(RANGE.id..string.format("Foul line distance = %.1f m.", fouldist)) + self:T(self.id..string.format("Foul line distance = %.1f m.", fouldist)) return fouldist end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event Handling +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- General event handler. -- @param #RANGE self @@ -1007,12 +1189,12 @@ function RANGE:onEvent(Event) end -- Event info. - self:T3(RANGE.id..string.format("EVENT: Event in onEvent with ID = %s", tostring(Event.id))) - self:T3(RANGE.id..string.format("EVENT: Ini unit = %s" , tostring(EventData.IniUnitName))) - self:T3(RANGE.id..string.format("EVENT: Ini group = %s" , tostring(EventData.IniGroupName))) - self:T3(RANGE.id..string.format("EVENT: Ini player = %s" , tostring(_playername))) - self:T3(RANGE.id..string.format("EVENT: Tgt unit = %s" , tostring(EventData.TgtUnitName))) - self:T3(RANGE.id..string.format("EVENT: Wpn type = %s" , tostring(EventData.WeaponTypeName))) + self:T3(self.id..string.format("EVENT: Event in onEvent with ID = %s", tostring(Event.id))) + self:T3(self.id..string.format("EVENT: Ini unit = %s" , tostring(EventData.IniUnitName))) + self:T3(self.id..string.format("EVENT: Ini group = %s" , tostring(EventData.IniGroupName))) + self:T3(self.id..string.format("EVENT: Ini player = %s" , tostring(_playername))) + self:T3(self.id..string.format("EVENT: Tgt unit = %s" , tostring(EventData.TgtUnitName))) + self:T3(self.id..string.format("EVENT: Wpn type = %s" , tostring(EventData.WeaponTypeName))) -- Call event Birth function. if Event.id==world.event.S_EVENT_BIRTH and _playername then @@ -1041,9 +1223,9 @@ function RANGE:OnEventBirth(EventData) local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - self:T3(RANGE.id.."BIRTH: unit = "..tostring(EventData.IniUnitName)) - self:T3(RANGE.id.."BIRTH: group = "..tostring(EventData.IniGroupName)) - self:T3(RANGE.id.."BIRTH: player = "..tostring(_playername)) + self:T3(self.id.."BIRTH: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.id.."BIRTH: group = "..tostring(EventData.IniGroupName)) + self:T3(self.id.."BIRTH: player = "..tostring(_playername)) if _unit and _playername then @@ -1054,26 +1236,26 @@ function RANGE:OnEventBirth(EventData) -- Debug output. local text=string.format("Player %s, callsign %s entered unit %s (UID %d) of group %s (GID %d)", _playername, _callsign, _unitName, _uid, _group:GetName(), _gid) - self:T(RANGE.id..text) + self:T(self.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) - self:_GetAmmo(_unitName) - -- Reset current strafe status. self.strafeStatus[_uid] = nil -- Add Menu commands after a delay of 0.1 seconds. - --self:_AddF10Commands(_unitName) SCHEDULER:New(nil, self._AddF10Commands, {self,_unitName}, 0.1) -- By default, some bomb impact points and do not flare each hit on target. - self.PlayerSettings[_playername]={} + self.PlayerSettings[_playername]={} --#RANGE.PlayerData 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 + self.PlayerSettings[_playername].unitname=_unitName + self.PlayerSettings[_playername].playername=_playername + self.PlayerSettings[_playername].airframe=EventData.IniUnit:GetTypeName() -- Start check in zone timer. if self.planes[_uid] ~= true then @@ -1091,9 +1273,9 @@ function RANGE:OnEventHit(EventData) self:F({eventhit = EventData}) -- Debug info. - self:T3(RANGE.id.."HIT: Ini unit = "..tostring(EventData.IniUnitName)) - self:T3(RANGE.id.."HIT: Ini group = "..tostring(EventData.IniGroupName)) - self:T3(RANGE.id.."HIT: Tgt target = "..tostring(EventData.TgtUnitName)) + self:T3(self.id.."HIT: Ini unit = "..tostring(EventData.IniUnitName)) + self:T3(self.id.."HIT: Ini group = "..tostring(EventData.IniGroupName)) + self:T3(self.id.."HIT: Tgt target = "..tostring(EventData.TgtUnitName)) -- Player info local _unitName = EventData.IniUnitName @@ -1141,7 +1323,7 @@ function RANGE:OnEventHit(EventData) 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) - self:T2(RANGE.id..text) + self:T2(self.id..text) _currentTarget.pastfoulline=true end end @@ -1203,12 +1385,13 @@ function RANGE:OnEventShot(EventData) local weaponcategory=desc.category -- Debug info. - self:T(RANGE.id.."EVENT SHOT: Range "..self.rangename) - self:T(RANGE.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) - self:T(RANGE.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) - self:T(RANGE.id.."EVENT SHOT: Weapon type = ".._weapon) - self:T(RANGE.id.."EVENT SHOT: Weapon name = ".._weaponName) - self:T(RANGE.id.."EVENT SHOT: Weapon cate = "..weaponcategory) + self:T(self.id.."EVENT SHOT: Range "..self.rangename) + self:T(self.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) + self:T(self.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) + self:T(self.id.."EVENT SHOT: Weapon type = ".._weapon) + self:T(self.id.."EVENT SHOT: Weapon name = ".._weaponName) + self:T(self.id.."EVENT SHOT: Weapon cate = "..weaponcategory) + -- Special cases: --local _viggen=string.match(_weapon, "ROBOT") or string.match(_weapon, "RB75") or string.match(_weapon, "BK90") or string.match(_weapon, "RB15") or string.match(_weapon, "RB04") @@ -1232,14 +1415,17 @@ function RANGE:OnEventShot(EventData) -- Distance player to range. if _unit and _playername then dPR=_unit:GetCoordinate():Get2DDistance(self.location) - self:T(RANGE.id..string.format("Range %s, player %s, player-range distance = %d km.", self.rangename, _playername, dPR/1000)) + self:T(self.id..string.format("Range %s, player %s, player-range distance = %d km.", self.rangename, _playername, dPR/1000)) end -- Only track if distance player to range is < 25 km. Also check that a player shot. No need to track AI weapons. if _track and dPR<=self.BombtrackThreshold and _unit and _playername then + + -- Player data. + local playerData=self.PlayerSettings[_playername] --#RANGE.PlayerData -- Tracking info and init of last bomb position. - self:T(RANGE.id..string.format("RANGE %s: Tracking %s - %s.", self.rangename, _weapon, EventData.weapon:getName())) + self:T(self.id..string.format("RANGE %s: Tracking %s - %s.", self.rangename, _weapon, EventData.weapon:getName())) -- Init bomb position. local _lastBombPos = {x=0,y=0,z=0} @@ -1253,22 +1439,30 @@ function RANGE:OnEventShot(EventData) return _ordnance:getPoint() end) - self:T3(RANGE.id..string.format("Range %s: Bomb still in air: %s", self.rangename, tostring(_status))) + self:T2(self.id..string.format("Range %s: Bomb still in air: %s", self.rangename, tostring(_status))) if _status then - -- Still in the air. Remember this position. + ---------------------------- + -- Weapon is still in air -- + ---------------------------- + + -- Remember this position. _lastBombPos = {x = _bombPos.x, y = _bombPos.y, z= _bombPos.z } - -- Check again in 0.005 seconds. + -- Check again in ~0.005 seconds ==> 200 checks per second. return timer.getTime() + self.dtBombtrack else - -- Bomb did hit the ground. + ----------------------------- + -- Bomb did hit the ground -- + ----------------------------- + -- Get closet target to last position. - local _closetTarget = nil - local _distance = nil - local _hitquality = "POOR" + local _closetTarget=nil --#RANGE.BombTarget + local _distance=nil + local _closeCoord=nil + local _hitquality="POOR" -- Get callsign. local _callsign=self:_myname(_unitName) @@ -1276,42 +1470,39 @@ function RANGE:OnEventShot(EventData) -- Coordinate of impact point. local impactcoord=COORDINATE:NewFromVec3(_lastBombPos) - -- Check if impact happend in range zone. + -- Check if impact happened in range zone. local insidezone=self.rangezone:IsCoordinateInZone(impactcoord) - -- Distance from range. We dont want to smoke targets outside of the range. - local impactdist=impactcoord:Get2DDistance(self.location) - -- Impact point of bomb. if self.Debug then impactcoord:MarkToAll("Bomb impact point") end -- Smoke impact point of bomb. - if self.PlayerSettings[_playername].smokebombimpact and insidezone then - if self.PlayerSettings[_playername].delaysmoke then - timer.scheduleFunction(self._DelayedSmoke, {coord=impactcoord, color=self.PlayerSettings[_playername].smokecolor}, timer.getTime() + self.TdelaySmoke) + if playerData.smokebombimpact and insidezone then + if playerData.delaysmoke then + timer.scheduleFunction(self._DelayedSmoke, {coord=impactcoord, color=playerData.smokecolor}, timer.getTime() + self.TdelaySmoke) else - impactcoord:Smoke(self.PlayerSettings[_playername].smokecolor) + impactcoord:Smoke(playerData.smokecolor) end end -- Loop over defined bombing targets. for _,_bombtarget in pairs(self.bombingTargets) do - local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE + -- Get target coordinate. + local targetcoord=self:_GetBombTargetCoordinate(_bombtarget) - if _target and _target:IsAlive() then + if targetcoord then -- Distance between bomb and target. - local _temp = impactcoord:Get2DDistance(_target:GetCoordinate()) - - --env.info(string.format("FF target = %s dist = %d m", _target:GetName(), _temp)) + local _temp = impactcoord:Get2DDistance(targetcoord) -- Find closest target to last known position of the bomb. if _distance == nil or _temp < _distance then _distance = _temp _closetTarget = _bombtarget + _closeCoord=targetcoord if _distance <= 0.5*_bombtarget.goodhitrange then _hitquality = "EXCELLENT" elseif _distance <= _bombtarget.goodhitrange then @@ -1327,32 +1518,43 @@ function RANGE:OnEventShot(EventData) end -- Count if bomb fell less than ~1 km away from the target. - if _distance <= self.scorebombdistance then - + if _distance and _distance <= self.scorebombdistance then -- Init bomb player results. if not self.bombPlayerResults[_playername] then - self.bombPlayerResults[_playername] = {} + self.bombPlayerResults[_playername]={} end -- Local results. - local _results = self.bombPlayerResults[_playername] + local _results=self.bombPlayerResults[_playername] + + local result={} --#RANGE.BombResult + result.name=_closetTarget.name or "unknown" + result.distance=_distance + result.radial=_closeCoord:HeadingTo(impactcoord) + result.weapon=_weaponName or "unknown" + result.quality=_hitquality + result.player=playerData.playername + result.time=timer.getAbsTime() + result.airframe=playerData.airframe -- Add to table. - table.insert(_results, {name=_closetTarget.name, distance =_distance, weapon = _weaponName, quality=_hitquality }) + table.insert(_results, result) + + -- Call impact. + self:Impact(result, playerData) - -- Send message to player. - local _message = string.format("%s, impact %d m from bullseye of target %s. %s hit.", _callsign, _distance, _closetTarget.name, _hitquality) - - -- Send message. - self:_DisplayMessageToGroup(_unit, _message, nil, true) elseif insidezone then - -- Send message + + -- Send message. local _message=string.format("%s, weapon fell more than %.1f km away from nearest range target. No score!", _callsign, self.scorebombdistance/1000) self:_DisplayMessageToGroup(_unit, _message, nil, false) + + else + self:T(self.id.."Weapon impacted outside range zone.") end --Terminate the timer - self:T(RANGE.id..string.format("Range %s, player %s: Terminating bomb track timer.", self.rangename, _playername)) + self:T(self.id..string.format("Range %s, player %s: Terminating bomb track timer.", self.rangename, _playername)) return nil end -- _status check @@ -1360,15 +1562,224 @@ function RANGE:OnEventShot(EventData) end -- end function trackBomb -- Weapon is not yet "alife" just yet. Start timer in one second. - self:T(RANGE.id..string.format("Range %s, player %s: Tracking of weapon starts in 0.1 seconds.", self.rangename, _playername)) + self:T(self.id..string.format("Range %s, player %s: Tracking of weapon starts in 0.1 seconds.", self.rangename, _playername)) timer.scheduleFunction(trackBomb, EventData.weapon, timer.getTime()+0.1) end --if _track (string.match) and player-range distance < threshold. end +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check spawn queue and spawn aircraft if necessary. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onafterStatus(From, Event, To) + + self:I(self.id..string.format("Range status: %s", self:GetState())) + + --self:_CheckMissileStatus() + + -- Save results. + if self.autosafe then + self:Save() + end + + self:__Status(-60) +end + + +--- Function called after bomb impact on range. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #RANGE.BombResult result Result of bomb impact. +-- @param #RANGE.PlayerData player +function RANGE:onafterImpact(From, Event, To, result, player) + + -- Only display target name if there is more than one bomb target. + local targetname=nil + if #self.bombingTargets>1 then + local targetname=result.name + end + + -- Send message to player. + local text=string.format("%s, impact %03d° for %d ft", player.playername, result.radial, UTILS.MetersToFeet(result.distance)) + if targetname then + text=text..string.format(" from bulls of target %s.") + else + text=text.."." + end + text=text..string.format(" %s hit.", result.quality) + + -- Unit. + local unit=UNIT:FindByName(player.unitname) + + -- Send message. + self:_DisplayMessageToGroup(unit, text, nil, true) + self:T(self.id..text) + +end + +--- Function called before save event. Checks that io and lfs are desanitized. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onbeforeSave(From, Event, To) + if io and lfs then + return true + else + self:E(self.id..string.format("WARNING: io and/or lfs not desanitized. Cannot save player results.")) + return false + end +end + +--- Function called after save. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onafterSave(From, Event, To) + + local function _savefile(filename, data) + local f=io.open(filename, "wb") + if f then + f:write(data) + f:close() + self:I(self.id..string.format("Saving player results to file %s", tostring(filename))) + else + self:E(self.id..string.format("ERROR: Could not save results to file %s", tostring(filename))) + end + end + + -- Path. + local path=lfs.writedir() + + -- Set file name. + local filename=path..string.format("\\RANGE-%s_BombingResults.csv", self.rangename) + + -- Header line. + local scores="Name,Pass,Target,Distance,Radial,Quality,Weapon,Airframe,Mission Time" + + -- Loop over all players. + for playername,results in pairs(self.bombPlayerResults) do + + -- Loop over player grades table. + for i,_result in pairs(results) do + local result=_result --#RANGE.BombResult + local distance=result.distance + local weapon=result.weapon + local target=result.name + local radial=result.radial + local quality=result.quality + local time=UTILS.SecondsToClock(result.time) + local airframe=result.airframe + scores=scores..string.format("\n%s,%d,%s,%.2f,%03d,%s,%s,%s,%s", playername, i, target, distance, radial, quality, weapon, airframe, time) + end + end + + _savefile(filename, scores) +end + +--- Function called before save event. Checks that io and lfs are desanitized. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onbeforeLoad(From, Event, To) + if io and lfs then + return true + else + self:E(self.id..string.format("WARNING: io and/or lfs not desanitized. Cannot load player results.")) + return false + end +end + +--- On after "Load" event. Loads results of all players from file. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onafterLoad(From, Event, To) + + --- Function that load data from a file. + local function _loadfile(filename) + local f=io.open(filename, "rb") + if f then + --self:I(self.id..string.format("Loading player results from file %s", tostring(filename))) + local data=f:read("*all") + f:close() + return data + else + self:E(self.id..string.format("ERROR: Could not load player results from file %s", tostring(filename))) + return nil + end + end + + -- Path. + local path=lfs.writedir() + + -- Set file name. + local filename=path..string.format("\\RANGE-%s_BombingResults.csv", self.rangename) + + -- Info message. + local text=string.format("Loading player bomb results from file %s", filename) + self:I(self.id..text) + + -- Load asset data from file. + local data=_loadfile(filename) + + if data then + + -- Split by line break. + local results=UTILS.Split(data,"\n") + + -- Remove first header line. + table.remove(results, 1) + + -- Init player scores table. + self.bombPlayerResults={} + + -- Loop over all lines. + for _,_result in pairs(results) do + + -- Parameters are separated by commata. + local resultdata=UTILS.Split(_result, ",") + + -- Grade table + local result={} --#RANGE.BombResult + + -- Player name. + local playername=resultdata[1] + result.player=playername + + -- Results data. + result.name=tostring(resultdata[3]) + result.distance=tonumber(resultdata[4]) + result.radial=tonumber(resultdata[5]) + result.quality=tostring(resultdata[6]) + result.weapon=tostring(resultdata[7]) + result.airframe=tostring(resultdata[8]) + result.time=UTILS.ClockToSeconds(resultdata[9] or "00:00:00") + + -- Create player array if necessary. + self.bombPlayerResults[playername]=self.bombPlayerResults[playername] or {} + + -- Add result to table. + table.insert(self.bombPlayerResults[playername], result) + end + end +end + ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Display Messages +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Start smoking a coordinate with a delay. -- @param #table _args Argements passed. @@ -1520,24 +1931,22 @@ function RANGE:_DisplayMyBombingResults(_unitName) -- Loop over results. local _bestMsg = "" - local _count = 1 - for _,_result in pairs(_results) do + for i,_result in pairs(_results) do + local result=_result --#RANGE.BombResult -- Message with name, weapon and distance. - _message = _message.."\n"..string.format("[%d] %d m - %s - %s - %s hit", _count, _result.distance, _result.name, _result.weapon, _result.quality) + _message = _message.."\n"..string.format("[%d] %d m %03d° - %s - %s - %s hit", i, result.distance, result.radial, result.name, result.weapon, result.quality) -- Store best/first result. if _bestMsg == "" then - _bestMsg = string.format("%d m - %s - %s - %s hit",_result.distance,_result.name,_result.weapon, _result.quality) + _bestMsg = string.format("%d m %03d° - %s - %s - %s hit", result.distance, result.radial, result.name, result.weapon, result.quality) end -- Best 10 runs only. - if _count == self.ndisplayresult then + if i==self.ndisplayresult then break end - -- Increase counter. - _count = _count+1 end -- Message. @@ -1625,8 +2034,12 @@ function RANGE:_DisplayRangeInfo(_unitname) if self.location then + local settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#SETTINGS + -- Direction vector from current position (coord) to target (position). local position=self.location --Core.Point#COORDINATE + local bulls=position:ToStringBULLS(unit:GetCoalition(), settings) + local lldms=position:ToStringLLDMS(settings) local rangealt=position:GetLandHeight() local vec3=coord:GetDirectionVec3(position) local angle=coord:GetAngleDegrees(vec3) @@ -1654,8 +2067,7 @@ function RANGE:_DisplayRangeInfo(_unitname) textdelay=string.format("Smoke bomb delay: OFF") end - -- Player unit settings. - local settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#SETTINGS + -- Player unit settings. local trange=string.format("%.1f km", range/1000) local trangealt=string.format("%d m", rangealt) local tstrafemaxalt=string.format("%d m", self.strafemaxalt) @@ -1669,6 +2081,8 @@ function RANGE:_DisplayRangeInfo(_unitname) text=text..string.format("Information on %s:\n", self.rangename) text=text..string.format("-------------------------------------------------------\n") text=text..string.format("Bearing %s, Range %s\n", Bs, trange) + text=text..string.format("%s\n", bulls) + text=text..string.format("%s\n", lldms) text=text..string.format("Altitude ASL: %s\n", trangealt) text=text..string.format("Max strafing alt AGL: %s\n", tstrafemaxalt) text=text..string.format("# of strafe targets: %d\n", self.nstrafetargets) @@ -1681,7 +2095,7 @@ function RANGE:_DisplayRangeInfo(_unitname) self:_DisplayMessageToGroup(unit, text, nil, true, true) -- Debug output. - self:T2(RANGE.id..text) + self:T2(self.id..text) end end end @@ -1706,12 +2120,14 @@ function RANGE:_DisplayBombTargets(_unitname) for _,_bombtarget in pairs(self.bombingTargets) do local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE - if _target and _target:IsAlive() then - -- Core.Point#COORDINATE - local coord=_target:GetCoordinate() --Core.Point#COORDINATE + -- Coordinate of bombtarget. + local coord=self:_GetBombTargetCoordinate(_bombtarget) + + if coord then + local mycoord=coord:ToStringA2G(_unit, _settings) - _text=_text..string.format("\n- %s: %s",_bombtarget.name, mycoord) + _text=_text..string.format("\n- %s: %s",_bombtarget.name or "unknown", mycoord) end end @@ -1752,7 +2168,7 @@ function RANGE:_DisplayStrafePits(_unitname) end local mycoord=coord:ToStringA2G(_unit, _settings) - _text=_text..string.format("\n- %s: %s - heading %03d",_strafepit.name, mycoord, heading) + _text=_text..string.format("\n- %s: %s - heading %03d°",_strafepit.name, mycoord, heading) end self:_DisplayMessageToGroup(_unit,_text, nil, true, true) @@ -1820,9 +2236,9 @@ function RANGE:_DisplayRangeWeather(_unitname) self:_DisplayMessageToGroup(unit, text, nil, true, true) -- Debug output. - self:T2(RANGE.id..text) + self:T2(self.id..text) else - self:T(RANGE.id..string.format("ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname)) + self:T(self.id..string.format("ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname)) end end @@ -1862,7 +2278,7 @@ function RANGE:_CheckInZone(_unitName) -- Debug output 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) + self:T2(self.id..text) -- Check if player is in strafe zone and below max alt. if unitinzone then @@ -1953,7 +2369,7 @@ function RANGE:_CheckInZone(_unitName) -- Debug info. local text=string.format("Checking zone %s. Unit = %s, player = %s in zone = %s. alt = %d, delta heading = %d", _targetZone.name, _unitName, _playername, tostring(unitinzone), unitalt, deltaheading) - self:T2(RANGE.id..text) + self:T2(self.id..text) -- Player is inside zone. if unitinzone then @@ -1983,6 +2399,7 @@ end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Menu Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Add menu commands for player. -- @param #RANGE self @@ -2077,16 +2494,55 @@ function RANGE:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(_gid, "Strafe Pits", _infoPath, self._DisplayStrafePits, self, _unitName) end else - self:T(RANGE.id.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) + self:T(self.id.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) end else - self:T(RANGE.id.."Player unit does not exist in AddF10Menu() function. Unit name: ".._unitName) + self:T(self.id.."Player unit does not exist in AddF10Menu() function. Unit name: ".._unitName) end end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Helper Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get the number of shells a unit currently has. +-- @param #RANGE self +-- @param #RANGE.BombTarget target Bomb target data. +-- @return Core.Point#COORDINATE Target coordinate. +function RANGE:_GetBombTargetCoordinate(target) + + local coord=nil --Core.Point#COORDINATE + + if target.type==RANGE.TargetType.UNIT then + + if not target.move then + -- Target should not move. + coord=target.coordinate + else + -- Moving target. Check if alive and get current position + if target.target and target.target:IsAlive() then + coord=target.target:GetCoordinate() + end + end + + elseif target.type==RANGE.TargetType.STATIC then + + -- Static targets dont move. + coord=target.coordinate + + elseif target.type==RANGE.TargetType.COORD then + + -- Coordinates dont move. + coord=target.coordinate + + else + self:E(self.id.."ERROR: Unknown target type.") + end + + return coord +end + --- Get the number of shells a unit currently has. -- @param #RANGE self @@ -2110,7 +2566,7 @@ function RANGE:_GetAmmo(unitname) if ammotable ~= nil then local weapons=#ammotable - self:T2(RANGE.id..string.format("Number of weapons %d.", weapons)) + self:T2(self.id..string.format("Number of weapons %d.", weapons)) for w=1,weapons do @@ -2124,11 +2580,11 @@ function RANGE:_GetAmmo(unitname) ammo=ammo+Nammo local text=string.format("Player %s has %d rounds ammo of type %s", playername, Nammo, Tammo) - self:T(RANGE.id..text) + self:T(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) else local text=string.format("Player %s has %d ammo of type %s", playername, Nammo, Tammo) - self:T(RANGE.id..text) + self:T(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) end end @@ -2197,8 +2653,8 @@ function RANGE:_IlluminateBombTargets(_unitName) for _,_bombtarget in pairs(self.bombingTargets) do local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE - if _target and _target:IsAlive() then - local coord=_target:GetCoordinate() --Core.Point#COORDINATE + local coord=self:_GetBombTargetCoordinate(_bombtarget) + if coord then table.insert(bomb, coord) end end @@ -2271,26 +2727,30 @@ function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear, display) _clear=true end - -- Group ID. - local _gid=_unit:GetGroup():GetID() + -- Check if unit is alive. + if _unit and _unit:IsAlive() then - -- Get playername and player settings - local _, playername=self:_GetPlayerUnitAndName(_unit:GetName()) - local playermessage=self.PlayerSettings[playername].messages + -- Group ID. + local _gid=_unit:GetGroup():GetID() + + -- 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 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) + -- Send message to examiner. + if self.examinergroupname~=nil then + local _examinerid=GROUP:FindByName(self.examinergroupname):GetID() + if _examinerid then + trigger.action.outTextForGroup(_examinerid, _text, _time, _clear) + end + end end - - -- Send message to examiner. - if self.examinergroupname~=nil then - local _examinerid=GROUP:FindByName(self.examinergroupname):GetID() - if _examinerid then - trigger.action.outTextForGroup(_examinerid, _text, _time, _clear) - end - end - + end --- Toggle status of smoking bomb impact points. @@ -2384,8 +2844,8 @@ function RANGE:_SmokeBombTargets(unitname) for _,_bombtarget in pairs(self.bombingTargets) do local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE - if _target and _target:IsAlive() then - local coord = _target:GetCoordinate() --Core.Point#COORDINATE + local coord=self:_GetBombTargetCoordinate(_bombtarget) + if coord then coord:Smoke(self.BombSmokeColor) end end @@ -2535,20 +2995,20 @@ function RANGE:_CheckStatic(name) -- If static is not yet in MOOSE DB, we add it. Can happen for cargo statics! if not _MOOSEstatic then - self:T(RANGE.id..string.format("Adding DCS static to MOOSE database. Name = %s.", name)) + self:T(self.id..string.format("Adding DCS static to MOOSE database. Name = %s.", name)) _DATABASE:AddStatic(name) end return true else - self:T3(RANGE.id..string.format("No static object with name %s exists.", name)) + self:T3(self.id..string.format("No static object with name %s exists.", name)) end -- Check if a unit has this name. if UNIT:FindByName(name) then return false else - self:T3(RANGE.id..string.format("No unit object with name %s exists.", name)) + self:T3(self.id..string.format("No unit object with name %s exists.", name)) end -- If not unit or static exist, we return nil. @@ -2620,21 +3080,4 @@ function RANGE:_myname(unitname) return string.format("%s (%s)", csign, pname) end ---- Split string. Cf http://stackoverflow.com/questions/1426954/split-string-in-lua --- @param #RANGE self --- @param #string str Sting to split. --- @param #string sep Speparator for split. --- @return #table Split text. -function RANGE:_split(str, sep) - self:F2({str=str, sep=sep}) - - local result = {} - local regex = ("([^%s]+)"):format(sep) - for each in str:gmatch(regex) do - table.insert(result, each) - end - - return result -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 792194f9b..8bc34ee99 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -227,6 +227,7 @@ -- @field #string trappath Path where to save the trap sheets. -- @field #string trapprefix File prefix for trap sheet files. -- @field #number initialmaxalt Max altitude in meters to register in the inital zone. +-- @field #boolean welcome If true, display welcome message to player. -- @extends Core.Fsm#FSM --- Be the boss! @@ -1225,6 +1226,7 @@ AIRBOSS = { trappath = nil, trapprefix = nil, initialmaxalt = nil, + welcome = nil, } --- Aircraft types capable of landing on carrier (human+AI). @@ -1671,7 +1673,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.9.9" +AIRBOSS.version="1.0.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1890,6 +1892,9 @@ function AIRBOSS:New(carriername, alias) self:SetMenuSmokeZones() self:SetMenuSingleCarrier(false) + -- Welcome players. + self:SetWelcomePlayers(true) + -- Init carrier parameters. if self.carriertype==AIRBOSS.CarrierType.STENNIS then self:_InitStennis() @@ -2224,6 +2229,18 @@ end -- USER API Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Set welcome messages for players. +-- @param #AIRBOSS self +-- @param #boolean switch If true, display welcome message to player. +-- @return #AIRBOSS self +function AIRBOSS:SetWelcomePlayers(switch) + + self.welcome=switch + + return self +end + + --- Set carrier controlled area (CCA). -- This is a large zone around the carrier, which is constantly updated wrt the carrier position. -- @param #AIRBOSS self @@ -7126,7 +7143,9 @@ function AIRBOSS:_NewPlayer(unitname) self.playerscores[playername]=self.playerscores[playername] or {} -- Welcome player message. - self:MessageToPlayer(playerData, string.format("Welcome, %s %s!", playerData.difficulty, playerData.name), string.format("AIRBOSS %s", self.alias), "", 5) + if self.welcome then + self:MessageToPlayer(playerData, string.format("Welcome, %s %s!", playerData.difficulty, playerData.name), string.format("AIRBOSS %s", self.alias), "", 5) + end end @@ -17071,15 +17090,11 @@ function AIRBOSS:onafterSave(From, Event, To, path, filename) filename=path.."\\"..filename end - -- Info - local text=string.format("Saving player LSO grades to file %s", filename) - MESSAGE:New(text,30):ToAllIf(self.Debug) - self:I(self.lid..text) - -- Header line local scores="Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case,Wind,Modex,Airframe,Carrier Type,Carrier Name,Theatre,Mission Time,Mission Date,OS Date\n" -- Loop over all players. + local n=0 for playername,grades in pairs(self.playerscores) do -- Loop over player grades table. @@ -17106,9 +17121,14 @@ function AIRBOSS:onafterSave(From, Event, To, path, filename) scores=scores..string.format("%s,%d,%s,%.1f,%s,%s,%s,%s,%d,%s,%s,%s,%s,%s,%s,%s,%s,%s\n", playername, i, finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, grade.case, grade.wind, grade.modex, grade.airframe, grade.carriertype, grade.carriername, grade.theatre, grade.mitime, grade.midate, grade.osdate) + n=n+1 end end + -- Info + local text=string.format("Saving %d player LSO grades to file %s", n, filename) + self:I(self.lid..text) + -- Save file. _savefile(filename, scores) end @@ -17219,6 +17239,7 @@ function AIRBOSS:onafterLoad(From, Event, To, path, filename) self.playerscores={} -- Loop over all lines. + local n=0 for _,gradeline in pairs(playergrades) do -- Parameters are separated by commata. @@ -17264,10 +17285,16 @@ function AIRBOSS:onafterLoad(From, Event, To, path, filename) -- Add grade to table. table.insert(self.playerscores[playername], grade) + n=n+1 + -- Debug info. self:T2({playername, self.playerscores[playername]}) end + -- Info message. + local text=string.format("Loaded %d player LSO grades from file %s", n, filename) + self:I(self.lid..text) + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ From 9bb1c83999a16a273af1510d9694c3e38d46ccb3 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 7 May 2019 22:00:07 +0200 Subject: [PATCH 2/3] MISC RANGE v2.1 - Added events when players enter/exit range zone. - misc RADIO - Added RADIOQUEUE class. WAREHOUSE v0.7.0 - Fixed bug in aircraft speed. - Decreased output if queues are empty. --- Moose Development/Moose/Core/Radio.lua | 477 +++++++++++++++++- Moose Development/Moose/Functional/Range.lua | 118 ++++- .../Moose/Functional/Warehouse.lua | 20 +- 3 files changed, 600 insertions(+), 15 deletions(-) diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index 9a2e6d570..ffd33255d 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -19,7 +19,7 @@ -- * Your sound files need to be encoded in **.ogg** or .wav, -- * Your sound files should be **as tiny as possible**. It is suggested you encode in .ogg with low bitrate and sampling settings, -- * They need to be added in .\l10n\DEFAULT\ in you .miz file (wich can be decompressed like a .zip file), --- * For simplicty sake, you can **let DCS' Mission Editor add the file** itself, by creating a new Trigger with the action "Sound to Country", and choosing your sound file and a country you don't use in your mission. +-- * For simplicity sake, you can **let DCS' Mission Editor add the file** itself, by creating a new Trigger with the action "Sound to Country", and choosing your sound file and a country you don't use in your mission. -- -- Due to weird DCS quirks, **radio communications behave differently** if sent by a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} or by any other @{Wrapper.Positionable#POSITIONABLE} -- @@ -28,7 +28,7 @@ -- -- Note that obviously, the **frequency** and the **modulation** of the transmission are important only if the players are piloting an **Advanced System Modelling** enabled aircraft, -- like the A10C or the Mirage 2000C. They will **hear the transmission** if they are tuned on the **right frequency and modulation** (and if they are close enough - more on that below). --- If a FC3 airacraft is used, it will **hear every communication, whatever the frequency and the modulation** is set to. The same is true for TACAN beacons. If your aircaft isn't compatible, +-- If an FC3 aircraft is used, it will **hear every communication, whatever the frequency and the modulation** is set to. The same is true for TACAN beacons. If your aircraft isn't compatible, -- you won't hear/be able to use the TACAN beacon informations. -- -- === @@ -39,7 +39,7 @@ -- @image Core_Radio.JPG ---- Models the radio capabilty. +--- Models the radio capability. -- -- ## RADIO usage -- @@ -56,12 +56,12 @@ -- * @{#RADIO.SetModulation}() : Sets the modulation of your transmission. -- * @{#RADIO.SetLoop}() : Choose if you want the transmission to be looped. If you need your transmission to be looped, you might need a @{#BEACON} instead... -- --- Additional Methods to set relevant parameters if the transmiter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} +-- Additional Methods to set relevant parameters if the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} -- -- * @{#RADIO.SetSubtitle}() : Set both the subtitle and its duration, -- * @{#RADIO.NewUnitTransmission}() : Shortcut to set all the relevant parameters in one method call -- --- Additional Methods to set relevant parameters if the transmiter is any other @{Wrapper.Positionable#POSITIONABLE} +-- Additional Methods to set relevant parameters if the transmitter is any other @{Wrapper.Positionable#POSITIONABLE} -- -- * @{#RADIO.SetPower}() : Sets the power of the antenna in Watts -- * @{#RADIO.NewGenericTransmission}() : Shortcut to set all the relevant parameters in one method call @@ -797,3 +797,470 @@ function BEACON:_TACANToFrequency(TACANChannel, TACANMode) return (A + TACANChannel - B) * 1000000 end +--- Manages radio transmissions. +-- +-- @type RADIOQUEUE +-- @field #string ClassName Name of the class "RADIOQUEUE". +-- @field #string lid ID for dcs.log. +-- @field #number frequency The radio frequency in Hz. +-- @field #number modulation The radio modulation. Either radio.modulation.AM or radio.modulation.FM. +-- @field Core.Scheduler#SCHEDULER scheduler The scheduler. +-- @field #string RQid The radio queue scheduler ID. +-- @field #table queue The queue of transmissions. +-- @field #number Tlast Time (abs) when the last transmission finished. +-- @field Core.Point#COORDINATE sendercoord Coordinate from where transmissions are broadcasted. +-- @field #number sendername Name of the sending unit or static. +-- @field Wrapper.Positionable#POSITIONABLE positionable The positionable to broadcast the message. +-- @field #number power Power of radio station in Watts. Default 100 W. +-- @field #table numbers Table of number transmission parameters. +-- @extends Core.Base#BASE +RADIOQUEUE = { + ClassName = "RADIOQUEUE", + lid=nil, + frequency=nil, + modulation=nil, + scheduler=nil, + RQid=nil, + queue={}, + Tlast=nil, + sendercoord=nil, + sendername=nil, + positionable=nil, + power=100, + numbers={}, +} + +--- Radio queue transmission data. +-- @type RADIOQUEUE.Transmission +-- @field #string filename Name of the file to be transmitted. +-- @field #string path Path in miz file where the file is located. +-- @field #number duration Duration in seconds. +-- @field #number Tstarted Mission time (abs) in seconds when the transmission started. +-- @field #boolean isplaying If true, transmission is currently playing. +-- @field #number Tplay Mission time (abs) in seconds when the transmission should be played. + + +--- Create a new RADIOQUEUE object for a given radio frequency/modulation. +-- @param #RADIOQUEUE self +-- @param #number frequency The radio frequency in MHz. +-- @param #number modulation (Optional) The radio modulation. Default radio.modulation.AM. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:New(frequency, modulation) + + -- Inherit base + local self=BASE:Inherit(self, BASE:New()) -- Core.Radio#RADIOQUEUE + + self.lid="RADIOQUEUE | " + + if frequency==nil then + self:E(self.lid.."ERROR: No frequency specified as first parameter!") + return nil + end + + -- Frequency in Hz. + self.frequency=frequency*1000000 + + -- Modulation. + self.modulation=modulation or radio.modulation.AM + + -- Scheduler + self.scheduler=SCHEDULER:New() + + return self +end + +--- Start the radio queue. +-- @param #RADIOQUEUE self +-- @param #number delay (Optional) Delay in seconds, before the radio queue is started. Default 1 sec. +-- @param #number dt (Optional) Time step in seconds for checking the queue. Default 0.01 sec. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:Start(delay, dt) + + delay=delay or 1 + + dt=dt or 0.01 + + self:I(self.lid..string.format("Starting RADIOQUEUE in %.1f seconds with interval dt=%.3f seconds.", delay, dt)) + + self.RQid=self.scheduler:Schedule(self, self._CheckRadioQueue, {}, delay, dt) + + return self +end + +--- Stop the radio queue. Stop scheduler and delete queue. +-- @param #RADIOQUEUE self +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:Stop() + self:I(self.lid.."Stopping RADIOQUEUE.") + self.scheduler:Stop(self.RQid) + self.queue={} + return self +end + +--- Set coordinate from where the transmission is broadcasted. +-- @param #RADIOQUEUE self +-- @param Core.Point#COORDINATE coordinate Coordinate of the sender. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetSenderCoordinate(coordinate) + self.sendercoord=coordinate + return self +end + +--- Set name of unit or static from which transmissions are made. +-- @param #RADIOQUEUE self +-- @param #string name Name of the unit or static used for transmissions. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetSenderUnitName(name) + self.sendername=name + return self +end + +--- Set parameters of a digit. +-- @param #RADIOQUEUE self +-- @param #number digit The digit 0-9. +-- @param #string filename The name of the sound file. +-- @param #number duration The duration of the sound file in seconds. +-- @param #string path The directory within the miz file where the sound is located. Default "l10n/DEFAULT/". +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetDigit(digit, filename, duration, path) + + local transmission={} --#RADIOQUEUE.Transmission + transmission.filename=filename + transmission.duration=duration + transmission.path=path or "l10n/DEFAULT/" + + -- Convert digit to string in case it is given as a number. + if type(digit)=="number" then + digit=tostring(digit) + end + + -- Set transmission. + self.numbers[digit]=transmission + + return self +end + +--- Add a transmission to the radio queue. +-- @param #RADIOQUEUE self +-- @param #RADIOQUEUE.Transmission transmission The transmission data table. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:AddTransmission(transmission) + self:F({transmission=transmission}) + + -- Init. + transmission.isplaying=false + transmission.Tstarted=nil + + -- Add to queue. + table.insert(self.queue, transmission) + + return self +end + +--- Add a transmission to the radio queue. +-- @param #RADIOQUEUE self +-- @param #string filename Name of the sound file. Usually an ogg or wav file type. +-- @param #number duration Duration in seconds the file lasts. +-- @param #number path Directory path inside the miz file where the sound file is located. Default "l10n/DEFAULT/". +-- @param #number tstart Start time (abs) seconds. Default now. +-- @param #number interval Interval in seconds after the last transmission finished. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:NewTransmission(filename, duration, path, tstart, interval) + + -- Sanity checks. + if not filename then + self:E(self.lid.."ERROR: No filename specified.") + return nil + end + if type(filename)~="string" then + self:E(self.lid.."ERROR: Filename specified is NOT a string.") + return nil + end + + if not duration then + self:E(self.lid.."ERROR: No duration of transmission specified.") + return nil + end + if type(duration)~="number" then + self:E(self.lid.."ERROR: Duration specified is NOT a number.") + return nil + end + + + local transmission={} --#RADIOQUEUE.Transmission + transmission.filename=filename + transmission.duration=duration + transmission.path=path or "l10n/DEFAULT/" + transmission.Tplay=tstart or timer.getAbsTime() + + -- Add transmission to queue. + self:AddTransmission(transmission) + + return self +end + +--- Convert a number (as string) into a radio transmission. +-- E.g. for board number or headings. +-- @param #RADIOQUEUE self +-- @param #string number Number string, e.g. "032" or "183". +-- @param #number delay Delay before transmission in seconds. +-- @param #number interval Interval between the next call. +-- @return #number Duration of the call in seconds. +function RADIOQUEUE:Number2Transmission(number, delay, interval) + + --- Split string into characters. + local function _split(str) + local chars={} + for i=1,#str do + local c=str:sub(i,i) + table.insert(chars, c) + end + return chars + end + + -- Split string into characters. + local numbers=_split(number) + + local wait=0 + for i=1,#numbers do + + -- Current number + local n=numbers[i] + + -- Radio call. + local transmission=UTILS.DeepCopy(self.numbers[n]) --#RADIOQUEUE.Transmission + + transmission.Tplay=timer.getAbsTime()+(delay or 0) + + if interval and i==1 then + transmission.interval=interval + end + + self:AddTransmission(transmission) + + -- Add up duration of the number. + wait=wait+transmission.duration + end + + -- Return the total duration of the call. + return wait +end + + +--- Broadcast radio message. +-- @param #RADIOQUEUE self +-- @param #RADIOQUEUE.Transmission transmission The transmission. +function RADIOQUEUE:Broadcast(transmission) + + -- Get unit sending the transmission. + local sender=self:_GetRadioSender() + + -- Construct file name. + local filename=string.format("%s%s", transmission.path, transmission.filename) + + -- Create subtitle for transmission. + --local subtitle=self:_RadioSubtitle(radio, call, loud) + + if sender then + + -- Broadcasting from aircraft. Only players tuned in to the right frequency will see the message. + self:T(self.lid..string.format("Broadcasting from aircraft %s", sender:GetName())) + + -- Command to set the Frequency for the transmission. + local commandFrequency={ + id="SetFrequency", + params={ + frequency=self.frequency, -- Frequency in Hz. + modulation=self.modulation, + }} + + -- Command to tranmit the call. + local commandTransmit={ + id = "TransmitMessage", + params = { + file=filename, + duration=transmission.subduration or 5, + subtitle=transmission.subtitle or "", + loop=false, + }} + + -- Set commend for frequency + sender:SetCommand(commandFrequency) + + -- Set command for radio transmission. + sender:SetCommand(commandTransmit) + + else + + -- Broadcasting from carrier. No subtitle possible. Need to send messages to players. + self:T(self.lid..string.format("Broadcasting from carrier via trigger.action.radioTransmission().")) + + -- Position from where to transmit. + local vec3=nil + + -- Try to get positon from sender unit/static. + if self.sendername then + local coord=self:_GetRadioSenderCoord() + if coord then + vec3=coord:GetVec3() + end + end + + -- Try to get fixed positon. + if self.sendercoord and not vec3 then + vec3=self.sendercoord:GetVec3() + end + + -- Transmit via trigger. + if vec3 then + trigger.action.radioTransmission(filename, vec3, self.modulation, false, self.frequency, self.power) + end + + end +end + +--- Check radio queue for transmissions to be broadcasted. +-- @param #RADIOQUEUE self +function RADIOQUEUE:_CheckRadioQueue() + + -- Check if queue is empty. + if #self.queue==0 then + return + end + + -- Get current abs time. + local time=timer.getAbsTime() + + local playing=false + local next=nil --#RADIOQUEUE.Transmission + local remove=nil + for i,_transmission in ipairs(self.queue) do + local transmission=_transmission --#RADIOQUEUE.Transmission + + -- Check if transmission time has passed. + if time>=transmission.Tplay then + + -- Check if transmission is currently playing. + if transmission.isplaying then + + -- Check if transmission is finished. + if time>=transmission.Tstarted+transmission.duration then + + -- Transmission over. + transmission.isplaying=false + + -- Remove ith element in queue. + remove=i + + -- Store time last transmission finished. + self.Tlast=time + + else -- still playing + + -- Transmission is still playing. + playing=true + + end + + else -- not playing yet + + local Tlast=self.Tlast + + if transmission.interval==nil then + + -- Not playing ==> this will be next. + if next==nil then + next=transmission + end + + else + + if time-Tlast>=transmission.interval then + next=transmission + else + + end + end + + -- We got a transmission or one with an interval that is not due yet. No need for anything else. + if next or Tlast then + break + end + + end + + else + + -- Transmission not due yet. + + end + end + + -- Found a new transmission. + if next~=nil and not playing then + self:Broadcast(next) + next.isplaying=true + next.Tstarted=time + end + + -- Remove completed calls from queue. + if remove then + table.remove(self.queue, remove) + end + +end + +--- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work. +-- @param #RADIOQUEUE self +-- @return Wrapper.Unit#UNIT Sending aircraft unit or nil if was not setup, is not an aircraft or is not alive. +function RADIOQUEUE:_GetRadioSender() + + -- Check if we have a sending aircraft. + local sender=nil --Wrapper.Unit#UNIT + + -- Try the general default. + if self.sendername then + -- First try to find a unit + sender=UNIT:FindByName(self.sendername) + + -- Check that sender is alive and an aircraft. + if sender and sender:IsAlive() and sender:IsAir() then + return sender + end + + end + + return nil +end + +--- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work. +-- @param #RADIOQUEUE self +-- @return Core.Point#COORDINATE Coordinate of the sender unit. +function RADIOQUEUE:_GetRadioSenderCoord() + + local vec3=nil + + -- Try the general default. + if self.sendername then + + -- First try to find a unit + local sender=UNIT:FindByName(self.sendername) + + -- Check that sender is alive and an aircraft. + if sender and sender:IsAlive() then + return sender:GetCoodinate() + end + + -- Now try a static. + local sender=STATIC:FindByName(self.sendername) + + -- Check that sender is alive and an aircraft. + if sender then + return sender:GetCoodinate() + end + + end + + return nil +end + diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 09c6be614..364a29660 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -52,6 +52,7 @@ -- @field #table bombingTargets Table of targets to bomb. -- @field #number nbombtargets Number of bombing targets. -- @field #number nstrafetargets Number of strafing targets. +-- @field #boolean messages Globally enable/disable all messages to players. -- @field #table MenuAddedTo Table for monitoring which players already got an F10 menu. -- @field #table planes Table for administration. -- @field #table strafeStatus Table containing the current strafing target a player as assigned to. @@ -217,6 +218,7 @@ RANGE={ id=nil, rangename=nil, location=nil, + messages=true, rangeradius=5000, rangezone=nil, strafeTargets={}, @@ -286,9 +288,11 @@ RANGE.TargetType={ -- @field #number smokecolor Color of smoke. -- @field #number flarecolor Color of flares. -- @field #boolean messages Display info messages. +-- @field Wrapper.Client#CLIENT client Client object of player. -- @field #string unitname Name of player aircraft unit. -- @field #string playername Name of player. -- @field #string airframe Aircraft type name. +-- @field #boolean inzone If true, player is inside the range zone. --- Bomb target data. -- @type RANGE.BombTarget @@ -336,7 +340,7 @@ RANGE.MenuF10Root=nil --- Range script version. -- @field #string version -RANGE.version="2.0.0" +RANGE.version="2.1.0" --TODO list: --TODO: Verbosity level for messages. @@ -388,6 +392,8 @@ function RANGE:New(rangename) self:AddTransition("Stopped", "Start", "Running") -- Start RANGE script. self:AddTransition("*", "Status", "*") -- Status of RANGE script. self:AddTransition("*", "Impact", "*") -- Impact of bomb/rocket/missile. + self:AddTransition("*", "EnterRange", "*") -- Player enters the range. + self:AddTransition("*", "ExitRange", "*") -- Player leaves the range. self:AddTransition("*", "Save", "*") -- Save player results. self:AddTransition("*", "Load", "*") -- Load player results. @@ -442,6 +448,44 @@ function RANGE:New(rangename) -- @param #string To To state. -- @param #RANGE.BombResult result Data of the bombing run. -- @param #RANGE.Playerdata player Data of player settings etc. + + --- Triggers the FSM event "EnterRange". + -- @function [parent=#RANGE] EnterRange + -- @param #RANGE self + -- @param #RANGE.Playerdata player Data of player settings etc. + + --- Triggers the FSM delayed event "EnterRange". + -- @function [parent=#RANGE] __EnterRange + -- @param #RANGE self + -- @param #number delay Delay in seconds before the function is called. + -- @param #RANGE.Playerdata player Data of player settings etc. + + --- On after "EnterRange" event user function. Called when a player enters the range zone. + -- @function [parent=#RANGE] OnAfterEnterRange + -- @param #RANGE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #RANGE.Playerdata player Data of player settings etc. + + --- Triggers the FSM event "ExitRange". + -- @function [parent=#RANGE] ExitRange + -- @param #RANGE self + -- @param #RANGE.Playerdata player Data of player settings etc. + + --- Triggers the FSM delayed event "ExitRange". + -- @function [parent=#RANGE] __ExitRange + -- @param #RANGE self + -- @param #number delay Delay in seconds before the function is called. + -- @param #RANGE.Playerdata player Data of player settings etc. + + --- On after "ExitRange" event user function. Called when a player leaves the range zone. + -- @function [parent=#RANGE] OnAfterExitRange + -- @param #RANGE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #RANGE.Playerdata player Data of player settings etc. -- Return object. return self @@ -721,6 +765,23 @@ function RANGE:DebugOFF() return self end +--- Disable ALL messages to players. +-- @param #RANGE self +-- @return #RANGE self +function RANGE:SetMessagesOFF() + self.messages=false + return self +end + +--- Enable messages to players. This is the default +-- @param #RANGE self +-- @return #RANGE self +function RANGE:SetMessagesON() + self.messages=true + return self +end + + --- Enables tracking of all bomb types. Note that this is the default setting. -- @param #RANGE self -- @return #RANGE self @@ -1253,9 +1314,11 @@ function RANGE:OnEventBirth(EventData) self.PlayerSettings[_playername].flarecolor=FLARECOLOR.Red self.PlayerSettings[_playername].delaysmoke=true self.PlayerSettings[_playername].messages=true + self.PlayerSettings[_playername].client=CLIENT:FindByName(_unitName, nil, true) self.PlayerSettings[_playername].unitname=_unitName self.PlayerSettings[_playername].playername=_playername self.PlayerSettings[_playername].airframe=EventData.IniUnit:GetTypeName() + self.PlayerSettings[_playername].inzone=false -- Start check in zone timer. if self.planes[_uid] ~= true then @@ -1580,16 +1643,19 @@ end -- @param #string To To state. function RANGE:onafterStatus(From, Event, To) + -- Check range status. self:I(self.id..string.format("Range status: %s", self:GetState())) - --self:_CheckMissileStatus() + -- Check player status. + self:_CheckPlayers() -- Save results. if self.autosafe then self:Save() end - self:__Status(-60) + -- Check back in ~10 seconds. + self:__Status(-10) end @@ -2245,6 +2311,47 @@ end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Timer Functions +--- Check status of players. +-- @param #RANGE self +-- @param #string _unitName Name of player unit. +function RANGE:_CheckPlayers() + + for playername,_playersettings in pairs(self.PlayerSettings) do + local playersettings=_playersettings --#RANGE.PlayerData + + local unitname=playersettings.unitname + local unit=UNIT:FindByName(unitname) + + if unit and unit:IsAlive() then + + if unit:IsInZone(self.rangezone) then + + ------------------------------ + -- Player INSIDE Range Zone -- + ------------------------------ + + if not playersettings.inzone then + self:EnterRange(playersettings) + playersettings.inzone=true + end + + else + + ------------------------------- + -- Player OUTSIDE Range Zone -- + ------------------------------- + + if playersettings.inzone==true then + self:ExitRange(playersettings) + playersettings.inzone=false + end + + end + end + end + +end + --- Check if player is inside a strafing zone. If he is, we start looking for hits. If he was and left the zone again, the result is stored. -- @param #RANGE self -- @param #string _unitName Name of player unit. @@ -2727,6 +2834,11 @@ function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear, display) _clear=true end + -- Messages globally disabled. + if self.messages==false then + return + end + -- Check if unit is alive. if _unit and _unit:IsAlive() then diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 3537c42d5..84fbb7cd0 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -1733,7 +1733,7 @@ _WAREHOUSEDB = { --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.6.9" +WAREHOUSE.version="0.7.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -7669,7 +7669,13 @@ function WAREHOUSE:_PrintQueue(queue, name) end - self:I(self.wid..text) + if #queue==0 then + self:T(self.wid..text) + else + if total~="Empty" then + self:I(self.wid..text) + end + end end --- Display status of warehouse. @@ -8109,28 +8115,28 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) --- Departure/Take-off c[#c+1]=Pdeparture - wp[#wp+1]=Pdeparture:WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, VxClimb, true, departure, nil, "Departure") + wp[#wp+1]=Pdeparture:WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, VxClimb*3.6, true, departure, nil, "Departure") --- Begin of Cruise local Pcruise=Pdeparture:Translate(d_climb, heading) Pcruise.y=FLcruise c[#c+1]=Pcruise - wp[#wp+1]=Pcruise:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxCruise, true, nil, nil, "Cruise") + wp[#wp+1]=Pcruise:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxCruise*3.6, true, nil, nil, "Cruise") --- Descent local Pdescent=Pcruise:Translate(d_cruise, heading) Pdescent.y=FLcruise c[#c+1]=Pdescent - wp[#wp+1]=Pdescent:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxDescent, true, nil, nil, "Descent") + wp[#wp+1]=Pdescent:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxDescent*3.6, true, nil, nil, "Descent") --- Holding point Pholding.y=H_holding+h_holding c[#c+1]=Pholding - wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding, true, nil, nil, "Holding") + wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding*3.6, true, nil, nil, "Holding") --- Final destination. c[#c+1]=Pdestination - wp[#wp+1]=Pdestination:WaypointAir("RADIO", COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, VxFinal, true, destination, nil, "Final Destination") + wp[#wp+1]=Pdestination:WaypointAir("RADIO", COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, VxFinal*3.6, true, destination, nil, "Final Destination") -- Mark points at waypoints for debugging. From 889ce8f64b2e6234a9acd0f0db4a4379a32db26f Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 23 May 2019 21:10:48 +0200 Subject: [PATCH 3/3] Fixed Database OnEventNewZone function --- Moose Development/Moose/Core/Base.lua | 22 ++++++++++++++++++++-- Moose Development/Moose/Core/Database.lua | 13 +------------ Moose Development/Moose/Core/Event.lua | 18 +++++++++++------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index a72fb0e1f..db2f5ddaf 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -595,18 +595,36 @@ do -- Event Handling -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when any unit begins firing a weapon that has a high rate of fire. Most common with aircraft cannons (GAU-8), autocannons, and machine guns. - -- initiator : The unit that is doing the shooing. + -- initiator : The unit that is doing the shooting. -- target: The unit that is being targeted. -- @function [parent=#BASE] OnEventShootingStart -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when any unit stops firing its weapon. Event will always correspond with a shooting start event. - -- initiator : The unit that was doing the shooing. + -- initiator : The unit that was doing the shooting. -- @function [parent=#BASE] OnEventShootingEnd -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. + --- Occurs when a new mark was added. + -- MarkID: ID of the mark. + -- @function [parent=#BASE] OnEventMarkAdded + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a mark was removed. + -- MarkID: ID of the mark. + -- @function [parent=#BASE] OnEventMarkRemoved + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a mark text was changed. + -- MarkID: ID of the mark. + -- @function [parent=#BASE] OnEventMarkChange + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + end diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 5a07d5f5a..0e87d5f7c 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -304,16 +304,6 @@ do -- Zones self.ZONES[ZoneName] = nil end - - --- Finds an @{Zone} based on the zone name in the DATABASE. - -- @param #DATABASE self - -- @param #string ZoneName - -- @return Core.Zone#ZONE_BASE The found @{Zone}. - function DATABASE:FindZone( ZoneName ) - - local ZoneFound = self.ZONES[ZoneName] - return ZoneFound - end --- Private method that registers new ZONE_BASE derived objects within the DATABASE Object. @@ -348,7 +338,6 @@ do -- Zones end -- zone - do -- cargo --- Adds a Cargo based on the Cargo Name in the DATABASE. @@ -1166,7 +1155,7 @@ function DATABASE:OnEventNewZone( EventData ) self:F2( { EventData } ) if EventData.Zone then - self:AddZone( EventData.Zone ) + self:AddZone( EventData.Zone.ZoneName, EventData.Zone ) end end diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index 386169cb0..3c00758eb 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -272,10 +272,16 @@ EVENTS = { -- @field Wrapper.Airbase#AIRBASE Place The MOOSE airbase object. -- @field #string PlaceName The name of the airbase. -- --- @field weapon The weapon used during the event. --- @field Weapon --- @field WeaponName --- @field WeaponTgtDCSUnit +-- @field #table weapon The weapon used during the event. +-- @field #table Weapon +-- @field #string WeaponName Name of the weapon. +-- @field DCS#Unit WeaponTgtDCSUnit Target DCS unit of the weapon. +-- +-- @field Cargo.Cargo#CARGO Cargo The cargo object. +-- @field #string CargoName The name of the cargo object. +-- +-- @field Core.ZONE#ZONE Zone The zone object. +-- @field #string ZoneName The name of the zone. @@ -959,8 +965,7 @@ function EVENT:onEvent( Event ) Event.PlaceName=Event.Place:GetName() end --- @FC: something like this should be added. ---[[ + -- Mark points. if Event.idx then Event.MarkID=Event.idx Event.MarkVec3=Event.pos @@ -969,7 +974,6 @@ function EVENT:onEvent( Event ) Event.MarkCoalition=Event.coalition Event.MarkGroupID = Event.groupID end -]] if Event.cargo then Event.Cargo = Event.cargo