diff --git a/Moose Development/Moose/Functional/Fox2.lua b/Moose Development/Moose/Functional/Fox2.lua index b5e9f5642..0d884b516 100644 --- a/Moose Development/Moose/Functional/Fox2.lua +++ b/Moose Development/Moose/Functional/Fox2.lua @@ -243,7 +243,7 @@ function FOX2:onafterStart(From, Event, To) self:TraceLevel(2) end - self:__Status(-1) + self:__Status(-10) end --- On after Stop event. Stops the missile trainer and unhandles events. diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 3c8037470..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. @@ -284,6 +288,7 @@ RANGE.TargetType={ -- @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 @@ -313,7 +318,8 @@ RANGE.TargetType={ -- @field #number radial Radial in degrees. -- @field #string weapon Name of the weapon. -- @field #string quality Hit quality. --- @field #RANGE.PlayerData player Player data. +-- @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. @@ -328,10 +334,6 @@ 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="2.0.0" @@ -366,9 +368,12 @@ function RANGE:New(rangename) --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:I(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 @@ -383,6 +388,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("*", "Save", "*") -- Save player results. + self:AddTransition("*", "Load", "*") -- Load player results. ------------------------ --- Pseudo Functions --- @@ -418,12 +425,14 @@ function RANGE:New(rangename) -- @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 @@ -432,6 +441,7 @@ function RANGE:New(rangename) -- @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 @@ -479,7 +489,7 @@ function RANGE:onafterStart() 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) + self:E(self.id..text) return end @@ -490,19 +500,19 @@ function RANGE:onafterStart() -- 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) + 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(RANGE.id.."Events are handled by 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(RANGE.id.."Events are handled directly by DCS.") + self:T(self.id.."Events are handled directly by DCS.") world.addEventHandler(self) end @@ -529,6 +539,7 @@ function RANGE:onafterStart() self.rangezone:SmokeZone(SMOKECOLOR.White) end + self:__Status(-60) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -562,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. @@ -777,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 @@ -810,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 @@ -880,7 +907,7 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe -- 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 @@ -952,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 @@ -992,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. @@ -1090,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. @@ -1100,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. @@ -1108,10 +1135,10 @@ 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 @@ -1162,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 @@ -1196,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 @@ -1209,7 +1236,7 @@ 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:E(RANGE.id..text) + self:T(self.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) -- Reset current strafe status. @@ -1228,6 +1255,7 @@ function RANGE:OnEventBirth(EventData) 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 @@ -1245,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 @@ -1295,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 @@ -1357,12 +1385,12 @@ 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") @@ -1387,7 +1415,7 @@ 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. @@ -1397,7 +1425,7 @@ function RANGE:OnEventShot(EventData) 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} @@ -1411,7 +1439,7 @@ function RANGE:OnEventShot(EventData) return _ordnance:getPoint() end) - self:T2(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 ---------------------------- @@ -1500,19 +1528,20 @@ function RANGE:OnEventShot(EventData) local _results=self.bombPlayerResults[_playername] local result={} --#RANGE.BombResult - result.name=_closetTarget.name + result.name=_closetTarget.name or "unknown" result.distance=_distance result.radial=_closeCoord:HeadingTo(impactcoord) - result.weapon=_weaponName + result.weapon=_weaponName or "unknown" result.quality=_hitquality - result.player=playerData + result.player=playerData.playername result.time=timer.getAbsTime() + result.airframe=playerData.airframe -- Add to table. table.insert(_results, result) -- Call impact. - self:Impact(result) + self:Impact(result, playerData) elseif insidezone then @@ -1525,7 +1554,7 @@ function RANGE:OnEventShot(EventData) 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 @@ -1533,7 +1562,7 @@ 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. @@ -1544,13 +1573,34 @@ 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. -function RANGE:onafterImpact(From, Event, To, result) +-- @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 @@ -1559,7 +1609,7 @@ function RANGE:onafterImpact(From, Event, To, result) end -- Send message to player. - local text=string.format("%s, impact %03d° for %d ft", result.player.playername, result.radial, UTILS.MetersToFeet(result.distance)) + 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 @@ -1568,7 +1618,7 @@ function RANGE:onafterImpact(From, Event, To, result) text=text..string.format(" %s hit.", result.quality) -- Unit. - local unit=UNIT:FindByName(result.player.unitname) + local unit=UNIT:FindByName(player.unitname) -- Send message. self:_DisplayMessageToGroup(unit, text, nil, true) @@ -1576,6 +1626,156 @@ function RANGE:onafterImpact(From, Event, To, result) 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 @@ -1731,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. @@ -1897,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 @@ -1970,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) @@ -2038,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 @@ -2080,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 @@ -2171,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 @@ -2296,10 +2494,10 @@ 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 @@ -2368,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 @@ -2382,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 @@ -2797,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. 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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------