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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------