From ac386d5ebefe8e0d92a79c50d3c4bebcbc9ee480 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 22 Jun 2023 09:34:02 +0200 Subject: [PATCH 01/25] #AIRBOSS * Bug fix --- Moose Development/Moose/Ops/Airboss.lua | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 3cc61f949..005f5b614 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -10266,7 +10266,7 @@ function AIRBOSS:_GetSternCoord() elseif case==2 or case==1 then -- V/Stol: Translate 8 meters port. self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(8, FB-90, true, true) - end + end elseif self.carriertype==AIRBOSS.CarrierType.STENNIS then -- Stennis: translate 7 meters starboard wrt Final bearing. self.sterncoord:Translate( self.carrierparam.sterndist, hdg, true, true ):Translate( 7, FB + 90, true, true ) @@ -11585,7 +11585,13 @@ function AIRBOSS:GetHeadingIntoWind( magnetic, coord ) end -- Get direction the wind is blowing from. This is where we want to go. - local windfrom, vwind = self:GetWind( nil, nil, coord ) + adjustDegreesForWindSpeed(vwind) + local windfrom, vwind = self:GetWind( nil, nil, coord ) + + --self:I("windfrom="..windfrom.." vwind="..vwind) + + vwind = vwind + adjustDegreesForWindSpeed(vwind) + + --self:I("windfrom="..windfrom.." (c)vwind="..vwind) -- Actually, we want the runway in the wind. local intowind = windfrom - self.carrierparam.rwyangle @@ -17348,7 +17354,7 @@ function AIRBOSS:_DisplayCarrierInfo( _unitname ) state = "Deck closed" end if self.turning then - state = state .. " (turning currently)" + state = state .. " (currently turning)" end -- Message text. From d937ab26795a1dd4a518c129db581a3e19bc38a7 Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Thu, 22 Jun 2023 13:46:11 +0200 Subject: [PATCH 02/25] CTLD (#1967) * Added option for troops subcategories in menu --- Moose Development/Moose/Ops/CTLD.lua | 42 +++++++++++++++++++++------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 298f7b1dd..7e79bdbc8 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -204,7 +204,7 @@ CTLD_CARGO = { -- @param #CTLD_CARGO self -- @param #boolean loaded function CTLD_CARGO:Isloaded() - if self.HasBeenMoved and not self.WasDropped() then + if self.HasBeenMoved and not self:WasDropped() then return true else return false @@ -1221,7 +1221,7 @@ CTLD.UnitTypes = { --- CTLD class version. -- @field #string version -CTLD.version="1.0.39" +CTLD.version="1.0.40" --- Instantiate a new CTLD. -- @param #CTLD self @@ -1390,6 +1390,7 @@ function CTLD:New(Coalition, Prefixes, Alias) -- sub categories self.usesubcats = false self.subcats = {} + self.subcatsTroop = {} -- disallow building in loadzones self.nobuildinloadzones = true @@ -2279,6 +2280,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) if not drop then inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) if not inzone then +---@diagnostic disable-next-line: cast-local-type inzone, ship, zone, distance, width = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) end else @@ -3468,6 +3470,12 @@ function CTLD:_RefreshF10Menus() self.subcats[entry.Subcategory] = entry.Subcategory end end + for _id,_cargo in pairs(self.Cargo_Troops) do + local entry = _cargo -- #CTLD_CARGO + if not self.subcatsTroop[entry.Subcategory] then + self.subcatsTroop[entry.Subcategory] = entry.Subcategory + end + end end -- build unit menus @@ -3504,15 +3512,28 @@ function CTLD:_RefreshF10Menus() local beaconself = MENU_GROUP_COMMAND:New(_group,"Drop beacon now",smoketopmenu, self.DropBeaconNow, self, _unit):Refresh() -- sub menus -- sub menu troops management - if cantroops then + if cantroops then local troopsmenu = MENU_GROUP:New(_group,"Load troops",toptroops) - for _,_entry in pairs(self.Cargo_Troops) do - local entry = _entry -- #CTLD_CARGO - menucount = menucount + 1 - menus[menucount] = MENU_GROUP_COMMAND:New(_group,entry.Name,troopsmenu,self._LoadTroops, self, _group, _unit, entry) + if self.usesubcats then + local subcatmenus = {} + for _name,_entry in pairs(self.subcatsTroop) do + subcatmenus[_name] = MENU_GROUP:New(_group,_name,troopsmenu) + end + for _,_entry in pairs(self.Cargo_Troops) do + local entry = _entry -- #CTLD_CARGO + local subcat = entry.Subcategory + menucount = menucount + 1 + menus[menucount] = MENU_GROUP_COMMAND:New(_group,entry.Name,subcatmenus[subcat],self._LoadTroops, self, _group, _unit, entry) + end + else + for _,_entry in pairs(self.Cargo_Troops) do + local entry = _entry -- #CTLD_CARGO + menucount = menucount + 1 + menus[menucount] = MENU_GROUP_COMMAND:New(_group,entry.Name,troopsmenu,self._LoadTroops, self, _group, _unit, entry) + end end local unloadmenu1 = MENU_GROUP_COMMAND:New(_group,"Drop troops",toptroops, self._UnloadTroops, self, _group, _unit):Refresh() - local extractMenu1 = MENU_GROUP_COMMAND:New(_group, "Extract troops", toptroops, self._ExtractTroops, self, _group, _unit):Refresh() + local extractMenu1 = MENU_GROUP_COMMAND:New(_group, "Extract troops", toptroops, self._ExtractTroops, self, _group, _unit):Refresh() end -- sub menu crates management if cancrates then @@ -3603,7 +3624,8 @@ end -- @param #number NoTroops Size of the group in number of Units across combined templates (for loading). -- @param #number PerTroopMass Mass in kg of each soldier -- @param #number Stock Number of groups in stock. Nil for unlimited. -function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops,PerTroopMass,Stock) +-- @param #string SubCategory Name of sub-category (optional). +function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops,PerTroopMass,Stock,SubCategory) self:T(self.lid .. " AddTroopsCargo") self:T({Name,Templates,Type,NoTroops,PerTroopMass,Stock}) if not self:_CheckTemplates(Templates) then @@ -3612,7 +3634,7 @@ function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops,PerTroopMass,Stock) end self.CargoCounter = self.CargoCounter + 1 -- Troops are directly loadable - local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,true,NoTroops,nil,nil,PerTroopMass,Stock) + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,true,NoTroops,nil,nil,PerTroopMass,Stock, SubCategory) table.insert(self.Cargo_Troops,cargo) return self end From 08be2d6e93e3269f6f9aa74101f16d6d5ee01d88 Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Thu, 22 Jun 2023 13:49:21 +0200 Subject: [PATCH 03/25] CTLD (#1967) (#1968) * Added option for troops subcategories in menu --- Moose Development/Moose/Ops/CTLD.lua | 42 +++++++++++++++++++++------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 298f7b1dd..7e79bdbc8 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -204,7 +204,7 @@ CTLD_CARGO = { -- @param #CTLD_CARGO self -- @param #boolean loaded function CTLD_CARGO:Isloaded() - if self.HasBeenMoved and not self.WasDropped() then + if self.HasBeenMoved and not self:WasDropped() then return true else return false @@ -1221,7 +1221,7 @@ CTLD.UnitTypes = { --- CTLD class version. -- @field #string version -CTLD.version="1.0.39" +CTLD.version="1.0.40" --- Instantiate a new CTLD. -- @param #CTLD self @@ -1390,6 +1390,7 @@ function CTLD:New(Coalition, Prefixes, Alias) -- sub categories self.usesubcats = false self.subcats = {} + self.subcatsTroop = {} -- disallow building in loadzones self.nobuildinloadzones = true @@ -2279,6 +2280,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) if not drop then inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) if not inzone then +---@diagnostic disable-next-line: cast-local-type inzone, ship, zone, distance, width = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) end else @@ -3468,6 +3470,12 @@ function CTLD:_RefreshF10Menus() self.subcats[entry.Subcategory] = entry.Subcategory end end + for _id,_cargo in pairs(self.Cargo_Troops) do + local entry = _cargo -- #CTLD_CARGO + if not self.subcatsTroop[entry.Subcategory] then + self.subcatsTroop[entry.Subcategory] = entry.Subcategory + end + end end -- build unit menus @@ -3504,15 +3512,28 @@ function CTLD:_RefreshF10Menus() local beaconself = MENU_GROUP_COMMAND:New(_group,"Drop beacon now",smoketopmenu, self.DropBeaconNow, self, _unit):Refresh() -- sub menus -- sub menu troops management - if cantroops then + if cantroops then local troopsmenu = MENU_GROUP:New(_group,"Load troops",toptroops) - for _,_entry in pairs(self.Cargo_Troops) do - local entry = _entry -- #CTLD_CARGO - menucount = menucount + 1 - menus[menucount] = MENU_GROUP_COMMAND:New(_group,entry.Name,troopsmenu,self._LoadTroops, self, _group, _unit, entry) + if self.usesubcats then + local subcatmenus = {} + for _name,_entry in pairs(self.subcatsTroop) do + subcatmenus[_name] = MENU_GROUP:New(_group,_name,troopsmenu) + end + for _,_entry in pairs(self.Cargo_Troops) do + local entry = _entry -- #CTLD_CARGO + local subcat = entry.Subcategory + menucount = menucount + 1 + menus[menucount] = MENU_GROUP_COMMAND:New(_group,entry.Name,subcatmenus[subcat],self._LoadTroops, self, _group, _unit, entry) + end + else + for _,_entry in pairs(self.Cargo_Troops) do + local entry = _entry -- #CTLD_CARGO + menucount = menucount + 1 + menus[menucount] = MENU_GROUP_COMMAND:New(_group,entry.Name,troopsmenu,self._LoadTroops, self, _group, _unit, entry) + end end local unloadmenu1 = MENU_GROUP_COMMAND:New(_group,"Drop troops",toptroops, self._UnloadTroops, self, _group, _unit):Refresh() - local extractMenu1 = MENU_GROUP_COMMAND:New(_group, "Extract troops", toptroops, self._ExtractTroops, self, _group, _unit):Refresh() + local extractMenu1 = MENU_GROUP_COMMAND:New(_group, "Extract troops", toptroops, self._ExtractTroops, self, _group, _unit):Refresh() end -- sub menu crates management if cancrates then @@ -3603,7 +3624,8 @@ end -- @param #number NoTroops Size of the group in number of Units across combined templates (for loading). -- @param #number PerTroopMass Mass in kg of each soldier -- @param #number Stock Number of groups in stock. Nil for unlimited. -function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops,PerTroopMass,Stock) +-- @param #string SubCategory Name of sub-category (optional). +function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops,PerTroopMass,Stock,SubCategory) self:T(self.lid .. " AddTroopsCargo") self:T({Name,Templates,Type,NoTroops,PerTroopMass,Stock}) if not self:_CheckTemplates(Templates) then @@ -3612,7 +3634,7 @@ function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops,PerTroopMass,Stock) end self.CargoCounter = self.CargoCounter + 1 -- Troops are directly loadable - local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,true,NoTroops,nil,nil,PerTroopMass,Stock) + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,true,NoTroops,nil,nil,PerTroopMass,Stock, SubCategory) table.insert(self.Cargo_Troops,cargo) return self end From f59326bf1065ef4ffce37dcff48f0d66c60d0b27 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 24 Jun 2023 14:03:43 +0200 Subject: [PATCH 04/25] #ATIS * Fix for airbases which have no runways, e.g. Naqoura Syria --- Moose Development/Moose/Ops/ATIS.lua | 31 ++++++++++++++++++---------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 3780ba573..719ad8da7 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -1,3 +1,4 @@ +---@diagnostic disable: cast-local-type --- **Ops** - Automatic Terminal Information Service (ATIS). -- -- === @@ -608,15 +609,16 @@ _ATIS = {} --- ATIS class version. -- @field #string version -ATIS.version = "0.9.14" +ATIS.version = "0.9.15" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Add new Normandy airfields. --- TODO: Zulu time --> Zulu in output. -- TODO: Correct fog for elevation. +-- DONE: Zulu time --> Zulu in output. +-- DONE: Fix for AB not having a runway - Helopost like Naqoura +-- DONE: Add new Normandy airfields. -- DONE: Use new AIRBASE system to set start/landing runway -- DONE: SetILS doesn't work -- DONE: Visibility reported twice over SRS @@ -2139,16 +2141,20 @@ function ATIS:onafterBroadcast( From, Event, To ) end end alltext = alltext .. ";\n" .. subtitle + + local _RUNACT if not self.ATISforFARPs then -- Active runway. - local subtitle=string.format("Active runway %s", runwayLanding) - if rwyLandingLeft==true then - subtitle=subtitle.." Left" - elseif rwyLandingLeft==false then - subtitle=subtitle.." Right" + if runwayLanding then + local subtitle=string.format("Active runway %s", runwayLanding) + if rwyLandingLeft==true then + subtitle=subtitle.." Left" + elseif rwyLandingLeft==false then + subtitle=subtitle.." Right" + end end - local _RUNACT = subtitle + _RUNACT = subtitle if not self.useSRS then self:Transmission(ATIS.Sound.ActiveRunway, 1.0, subtitle) self.radioqueue:Number2Transmission(runwayLanding) @@ -2509,8 +2515,11 @@ function ATIS:GetActiveRunway(Takeoff) else runway=self.airbase:GetActiveRunwayLanding() end - - return runway.name, runway.isLeft + if runway then -- some ABs have NO runways, e.g. Syria Naqoura + return runway.name, runway.isLeft + else + return nil, nil + end end --- Get runway from user supplied magnetic heading. From a978420a67208a825f481d1e544892ce1d0ea6bc Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Mon, 26 Jun 2023 13:25:23 +0200 Subject: [PATCH 05/25] Point - BRAANATO (#1969) corrected Track to be direction of travel of bogey (self in this case) --- Moose Development/Moose/Core/Point.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index d942e49ba..e699af694 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -2944,8 +2944,13 @@ do -- COORDINATE if alt < 1 then alttext = "very low" end - - local track = UTILS.BearingToCardinal(bearing) or "North" + + -- corrected Track to be direction of travel of bogey (self in this case) + local track = "Maneuver" + + if self.Heading then + track = UTILS.BearingToCardinal(self.Heading) or "North" + end if rangeNM > 3 then if SSML then -- google says "oh" instead of zero, be aware From 0dab316514cbe4c0f19b78fe4803645182413b92 Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Mon, 26 Jun 2023 13:29:34 +0200 Subject: [PATCH 06/25] Point - BRAANATO (#1969) (#1970) corrected Track to be direction of travel of bogey (self in this case) --- Moose Development/Moose/Core/Point.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index d942e49ba..e699af694 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -2944,8 +2944,13 @@ do -- COORDINATE if alt < 1 then alttext = "very low" end - - local track = UTILS.BearingToCardinal(bearing) or "North" + + -- corrected Track to be direction of travel of bogey (self in this case) + local track = "Maneuver" + + if self.Heading then + track = UTILS.BearingToCardinal(self.Heading) or "North" + end if rangeNM > 3 then if SSML then -- google says "oh" instead of zero, be aware From fd946e971a8db803634e7b532496d185e932c2d6 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 1 Jul 2023 13:08:27 +0200 Subject: [PATCH 07/25] #AWACS * Bugfixes --- Moose Development/Moose/Ops/Awacs.lua | 29 +++++++++++++++------------ 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/Moose Development/Moose/Ops/Awacs.lua b/Moose Development/Moose/Ops/Awacs.lua index 7da32be2f..7eaf70fa1 100644 --- a/Moose Development/Moose/Ops/Awacs.lua +++ b/Moose Development/Moose/Ops/Awacs.lua @@ -499,7 +499,7 @@ do -- @field #AWACS AWACS = { ClassName = "AWACS", -- #string - version = "0.2.54", -- #string + version = "0.2.55", -- #string lid = "", -- #string coalition = coalition.side.BLUE, -- #number coalitiontxt = "blue", -- #string @@ -783,8 +783,8 @@ AWACS.Messages = { -- @field #string AwacsStateMission -- @field #string AwacsStateFG -- @field #boolean AwacsShiftChange --- @field #string EscortsStateMission --- @field #string EscortsStateFG +-- @field #table EscortsStateMission +-- @field #table EscortsStateFG -- @field #boolean EscortsShiftChange -- @field #number AICAPMax -- @field #number AICAPCurrent @@ -1162,8 +1162,8 @@ function AWACS:New(Name,AirWing,Coalition,AirbaseName,AwacsOrbit,OpsZone,Station MonitoringData.AwacsStateFG = "unknown" MonitoringData.AwacsStateMission = "unknown" MonitoringData.EscortsShiftChange = false - MonitoringData.EscortsStateFG= "unknown" - MonitoringData.EscortsStateMission = "unknown" + MonitoringData.EscortsStateFG = {} + MonitoringData.EscortsStateMission = {} self.MonitoringOn = false -- #boolean self.MonitoringData = MonitoringData @@ -2027,9 +2027,9 @@ function AWACS:_StartEscorts(Shiftchange) self.CatchAllMissions[#self.CatchAllMissions+1] = escort if Shiftchange then - self.EscortMissionReplacement[i] = mission + self.EscortMissionReplacement[i] = escort else - self.EscortMission[i] = mission + self.EscortMission[i] = escort end end @@ -3597,10 +3597,13 @@ function AWACS:_SetClientMenus() local tasking = MENU_GROUP:New(cgrp,"Tasking",basemenu) local showtask = MENU_GROUP_COMMAND:New(cgrp,"Showtask",tasking,self._Showtask,self,cgrp) + local commit + local unable + local abort if self.PlayerCapAssignment then - local commit = MENU_GROUP_COMMAND:New(cgrp,"Commit",tasking,self._Commit,self,cgrp) - local unable = MENU_GROUP_COMMAND:New(cgrp,"Unable",tasking,self._Unable,self,cgrp) - local abort = MENU_GROUP_COMMAND:New(cgrp,"Abort",tasking,self._TaskAbort,self,cgrp) + commit = MENU_GROUP_COMMAND:New(cgrp,"Commit",tasking,self._Commit,self,cgrp) + unable = MENU_GROUP_COMMAND:New(cgrp,"Unable",tasking,self._Unable,self,cgrp) + abort = MENU_GROUP_COMMAND:New(cgrp,"Abort",tasking,self._TaskAbort,self,cgrp) --local judy = MENU_GROUP_COMMAND:New(cgrp,"Judy",tasking,self._Judy,self,cgrp) end @@ -4933,8 +4936,8 @@ function AWACS:_AnnounceContact(Contact,IsNew,Group,IsBogeyDope,Tag,IsPopup,Repo end end - string.gsub(BRAText,"BRAA","brah") - string.gsub(BRAText,"BRA","brah") + BRAText = string.gsub(BRAText,"BRAA","brah") + BRAText = string.gsub(BRAText,"BRA","brah") local prio = IsNew or IsBogeyDope self:_NewRadioEntry(BRAText,TextScreen,GID,isGroup,true,IsNew,false,prio) @@ -5547,7 +5550,7 @@ end -- @param #string Event -- @param #string To -- @return #AWACS self -function AWACS:onbeforeStart(From,Event,to) +function AWACS:onbeforeStart(From,Event,To) self:T({From, Event, To}) if self.IncludeHelicopters then self.clientset:FilterCategories("helicopter") From 0bb16ec8271af98dda9ed3094c0d2b30acfd6100 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 1 Jul 2023 13:09:16 +0200 Subject: [PATCH 08/25] #AIRBASE * Fix for nil error in finding parking for a group #MANTIS * Added SAM type "SHORAD" as designator #SCORING * fix for non local problem #UTILS * fix for OneLineSerialize --- Moose Development/Moose/Functional/Mantis.lua | 7 ++++--- Moose Development/Moose/Functional/Scoring.lua | 6 +++--- Moose Development/Moose/Utilities/Utils.lua | 4 ++-- Moose Development/Moose/Wrapper/Airbase.lua | 18 +++++++++--------- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/Moose Development/Moose/Functional/Mantis.lua b/Moose Development/Moose/Functional/Mantis.lua index 3f6f7b008..aeef55fa0 100644 --- a/Moose Development/Moose/Functional/Mantis.lua +++ b/Moose Development/Moose/Functional/Mantis.lua @@ -22,7 +22,7 @@ -- @module Functional.Mantis -- @image Functional.Mantis.jpg -- --- Last Update: Oct 2022 +-- Last Update: July 2023 ------------------------------------------------------------------------- --- **MANTIS** class, extends Core.Base#BASE @@ -369,6 +369,7 @@ MANTIS.SamData = { ["SA-20A"] = { Range=150, Blindspot=5, Height=27, Type="Long" , Radar="S-300PMU1"}, ["SA-20B"] = { Range=200, Blindspot=4, Height=27, Type="Long" , Radar="S-300PMU2"}, ["HQ-2"] = { Range=50, Blindspot=6, Height=35, Type="Medium", Radar="HQ_2_Guideline_LN" }, + ["SHORAD"] = { Range=3, Blindspot=0, Height=3, Type="Short", Radar="Igla" } } --- SAM data HDS @@ -578,7 +579,7 @@ do -- TODO Version -- @field #string version - self.version="0.8.9" + self.version="0.8.10" self:I(string.format("***** Starting MANTIS Version %s *****", self.version)) --- FSM Functions --- @@ -1319,7 +1320,7 @@ do elseif sma then SAMData = self.SamDataSMA end - --self:I("Looking to auto-match for "..grpname) + --self:T("Looking to auto-match for "..grpname) for _,_unit in pairs(units) do local unit = _unit -- Wrapper.Unit#UNIT local type = string.lower(unit:GetTypeName()) diff --git a/Moose Development/Moose/Functional/Scoring.lua b/Moose Development/Moose/Functional/Scoring.lua index 950ccd3c0..48841a177 100644 --- a/Moose Development/Moose/Functional/Scoring.lua +++ b/Moose Development/Moose/Functional/Scoring.lua @@ -1767,9 +1767,9 @@ function SCORING:SecondsToClock( sSeconds ) -- return nil; return "00:00:00"; else - nHours = string.format( "%02.f", math.floor( nSeconds / 3600 ) ); - nMins = string.format( "%02.f", math.floor( nSeconds / 60 - (nHours * 60) ) ); - nSecs = string.format( "%02.f", math.floor( nSeconds - nHours * 3600 - nMins * 60 ) ); + local nHours = string.format( "%02.f", math.floor( nSeconds / 3600 ) ); + local nMins = string.format( "%02.f", math.floor( nSeconds / 60 - (nHours * 60) ) ); + lcoal nSecs = string.format( "%02.f", math.floor( nSeconds - nHours * 3600 - nMins * 60 ) ); return nHours .. ":" .. nMins .. ":" .. nSecs end end diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 891718da7..99c643a7a 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -299,14 +299,14 @@ end -- @param #table tbl Input table. UTILS.OneLineSerialize = function( tbl ) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function - lookup_table = {} +local lookup_table = {} local function _Serialize( tbl ) if type(tbl) == 'table' then --function only works for tables! if lookup_table[tbl] then - return lookup_table[object] + return lookup_table[tbl] end local tbl_str = {} diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 66aca8d82..276ec5b8d 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -12,7 +12,7 @@ -- @image Wrapper_Airbase.JPG ---- @type AIRBASE +-- @type AIRBASE -- @field #string ClassName Name of the class, i.e. "AIRBASE". -- @field #table CategoryName Names of airbase categories. -- @field #string AirbaseName Name of the airbase. @@ -1501,16 +1501,16 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, -- Get the aircraft size, i.e. it's longest side of x,z. local aircraft = nil -- fix local problem below - local _aircraftsize, ax,ay,az + -- SU27 dimensions as default + local _aircraftsize = 23 + local ax = 23 -- l + local ay = 7 -- h + local az = 17 -- w if group and group.ClassName == "GROUP" then aircraft=group:GetUnit(1) - _aircraftsize, ax,ay,az=aircraft:GetObjectSize() - else - -- SU27 dimensions - _aircraftsize = 23 - ax = 23 -- length - ay = 7 -- height - az = 17 -- width + if aircraft then + _aircraftsize, ax,ay,az=aircraft:GetObjectSize() + end end From 1eaa3d309df58a65631d9f1b0e28937798d7e8b7 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 1 Jul 2023 13:15:56 +0200 Subject: [PATCH 09/25] #SCORING * typo --- Moose Development/Moose/Functional/Scoring.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Functional/Scoring.lua b/Moose Development/Moose/Functional/Scoring.lua index 48841a177..3d0c56b9f 100644 --- a/Moose Development/Moose/Functional/Scoring.lua +++ b/Moose Development/Moose/Functional/Scoring.lua @@ -1769,7 +1769,7 @@ function SCORING:SecondsToClock( sSeconds ) else local nHours = string.format( "%02.f", math.floor( nSeconds / 3600 ) ); local nMins = string.format( "%02.f", math.floor( nSeconds / 60 - (nHours * 60) ) ); - lcoal nSecs = string.format( "%02.f", math.floor( nSeconds - nHours * 3600 - nMins * 60 ) ); + local nSecs = string.format( "%02.f", math.floor( nSeconds - nHours * 3600 - nMins * 60 ) ); return nHours .. ":" .. nMins .. ":" .. nSecs end end From 8c8ef19f01b83f95a4a07190705fc3b0f1477ed4 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 3 Jul 2023 16:44:46 +0200 Subject: [PATCH 10/25] #CONTROLLABLE * Added TaskGroundEscort --- .../Moose/Wrapper/Controllable.lua | 54 ++++++++++++++++--- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index ba53f6f60..37067d492 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -52,6 +52,7 @@ -- * @{#CONTROLLABLE.TaskEmbarking}: (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. -- * @{#CONTROLLABLE.TaskEmbarkToTransport}: (GROUND) Embark to a Transport landed at a location. -- * @{#CONTROLLABLE.TaskEscort}: (AIR) Escort another airborne controllable. +-- * @{#CONTROLLABLE.TaskGroundEscort}: (AIR/HELO) Escort a ground controllable. -- * @{#CONTROLLABLE.TaskFAC_AttackGroup}: (AIR + GROUND) The task makes the controllable/unit a FAC and orders the FAC to control the target (enemy ground controllable) destruction. -- * @{#CONTROLLABLE.TaskFireAtPoint}: (GROUND) Fire some or all ammunition at a VEC2 point. -- * @{#CONTROLLABLE.TaskFollow}: (AIR) Following another airborne controllable. @@ -1270,7 +1271,7 @@ end --- (AIR) Orbit at a position with at a given altitude and speed. Optionally, a race track pattern can be specified. -- @param #CONTROLLABLE self --- @param Core.Point#COORDINATE Coord Coordinate at which the CONTROLLABLE orbits. +-- @param Core.Point#COORDINATE Coord Coordinate at which the CONTROLLABLE orbits. Can also be given as a `DCS#Vec3` or `DCS#Vec2` object. -- @param #number Altitude Altitude in meters of the orbit pattern. Default y component of Coord. -- @param #number Speed Speed [m/s] flying the orbit pattern. Default 128 m/s = 250 knots. -- @param Core.Point#COORDINATE CoordRaceTrack (Optional) If this coordinate is specified, the CONTROLLABLE will fly a race-track pattern using this and the initial coordinate. @@ -1279,11 +1280,11 @@ function CONTROLLABLE:TaskOrbit( Coord, Altitude, Speed, CoordRaceTrack ) local Pattern = AI.Task.OrbitPattern.CIRCLE - local P1 = Coord:GetVec2() + local P1 = {x=Coord.x, y=Coord.z or Coord.y} local P2 = nil if CoordRaceTrack then Pattern = AI.Task.OrbitPattern.RACE_TRACK - P2 = CoordRaceTrack:GetVec2() + P2 = {x=CoordRaceTrack.x, y=CoordRaceTrack.z or CoordRaceTrack.y} end local Task = { @@ -1480,15 +1481,53 @@ function CONTROLLABLE:TaskFollow( FollowControllable, Vec3, LastWaypointIndex ) return DCSTask end +--- (AIR/HELO) Escort a ground controllable. +-- The unit / controllable will follow lead unit of the other controllable, additional units of both controllables will continue following their leaders. +-- The unit / controllable will also protect that controllable from threats of specified types. +-- @param #CONTROLLABLE self +-- @param #CONTROLLABLE FollowControllable The controllable to be escorted. +-- @param #number LastWaypointIndex (optional) Detach waypoint of another controllable. Once reached the unit / controllable Escort task is finished. +-- @param #number OrbitDistance (optional) Maximum distance helo will orbit around the ground unit in meters. Defaults to 2000 meters. +-- @param DCS#AttributeNameArray TargetTypes (optional) Array of AttributeName that is contains threat categories allowed to engage. Default {"Ground vehicles"}. See [https://wiki.hoggit.us/view/DCS_enum_attributes](https://wiki.hoggit.us/view/DCS_enum_attributes) +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskGroundEscort( FollowControllable, LastWaypointIndex, OrbitDistance, TargetTypes ) + + -- Escort = { + -- id = 'GroundEscort', + -- params = { + -- groupId = Group.ID, -- must + -- engagementDistMax = Distance, -- Must. With his task it does not appear to actually define the range AI are allowed to attack at, rather it defines the size length of the orbit. The helicopters will fly up to this set distance before returning to the escorted group. + -- lastWptIndexFlag = boolean, -- optional + -- lastWptIndex = number, -- optional + -- targetTypes = array of AttributeName, -- must + -- lastWptIndexFlagChangedManually = boolean, -- must be true + -- } + -- } + + local DCSTask = { + id = 'GroundEscort', + params = { + groupId = FollowControllable and FollowControllable:GetID() or nil, + engagementDistMax = OrbitDistance or 2000, + lastWptIndexFlag = LastWaypointIndex and true or false, + lastWptIndex = LastWaypointIndex, + targetTypes = TargetTypes or {"Ground vehicles"}, + lastWptIndexFlagChangedManually = true, + }, + } + + return DCSTask +end + --- (AIR) Escort another airborne controllable. -- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. -- The unit / controllable will also protect that controllable from threats of specified types. -- @param #CONTROLLABLE self -- @param #CONTROLLABLE FollowControllable The controllable to be escorted. -- @param DCS#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. --- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. --- @param #number EngagementDistance Maximal distance from escorted controllable to threat. If the threat is already engaged by escort escort will disengage if the distance becomes greater than 1.5 * engagementDistMax. --- @param DCS#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. Default {"Air"}. +-- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Escort task is finished. +-- @param #number EngagementDistance Maximal distance from escorted controllable to threat in meters. If the threat is already engaged by escort escort will disengage if the distance becomes greater than 1.5 * engagementDistMax. +-- @param DCS#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. Default {"Air"}. See https://wiki.hoggit.us/view/DCS_enum_attributes -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes ) @@ -1504,8 +1543,7 @@ function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, E -- } -- } - local DCSTask - DCSTask = { + local DCSTask = { id = 'Escort', params = { groupId = FollowControllable and FollowControllable:GetID() or nil, From 71b2cc1ee55fb5d1c001dbd28bcf7eaf6ba846e2 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 3 Jul 2023 17:06:31 +0200 Subject: [PATCH 11/25] #AMMOTRUCK * Added TTL --- .../Moose/Functional/AmmoTruck.lua | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/Moose Development/Moose/Functional/AmmoTruck.lua b/Moose Development/Moose/Functional/AmmoTruck.lua index e33ae8a52..83b177bef 100644 --- a/Moose Development/Moose/Functional/AmmoTruck.lua +++ b/Moose Development/Moose/Functional/AmmoTruck.lua @@ -17,7 +17,7 @@ -- @module Functional.AmmoTruck -- @image Artillery.JPG -- --- Date: Nov 2022 +-- Last update: July 2023 ------------------------------------------------------------------------- --- **AMMOTRUCK** class, extends Core.FSM#FSM @@ -40,6 +40,7 @@ -- @field #number unloadtime Unload time in seconds -- @field #number waitingtime Max waiting time in seconds -- @field #boolean routeonroad Route truck on road if true (default) +-- @field #number reloads Number of reloads a single truck can do before he must return home -- @extends Core.FSM#FSM --- *Amateurs talk about tactics, but professionals study logistics.* - General Robert H Barrow, USMC @@ -73,9 +74,10 @@ -- ammotruck.remunidist = 20000 -- 20km - send trucks max this far from home -- ammotruck.unloadtime = 600 -- 10 minutes - min time to unload ammunition -- ammotruck.waitingtime = 1800 -- 30 mintes - wait max this long until remunition is done --- ammotruck.monitor = -60 - 1 minute - AMMOTRUCK checks on things every 1 minute --- ammotruck.routeonroad = true - Trucks will **try** to drive on roads --- ammotruck.usearmygroup = false - if true, will make use of ARMYGROUP in the background (if used in DEV branch) +-- ammotruck.monitor = -60 -- 1 minute - AMMOTRUCK checks run every one minute +-- ammotruck.routeonroad = true -- Trucks will **try** to drive on roads +-- ammotruck.usearmygroup = false -- If true, will make use of ARMYGROUP in the background (if used in DEV branch) +-- ammotruck.reloads = 5 -- Maxn re-arms a truck can do before he needs to go home and restock. Set to -1 for unlimited -- -- ## 3 FSM Events to shape mission -- @@ -113,7 +115,7 @@ AMMOTRUCK = { ClassName = "AMMOTRUCK", lid = "", - version = "0.0.10", + version = "0.0.12", alias = "", debug = false, trucklist = {}, @@ -128,7 +130,8 @@ AMMOTRUCK = { monitor = -60, unloadtime = 600, waitingtime = 1800, - routeonroad = true + routeonroad = true, + reloads = 5, } --- @@ -156,6 +159,7 @@ AMMOTRUCK.State = { --@field #string targetname --@field Wrapper.Group#GROUP targetgroup --@field Core.Point#COORDINATE targetcoordinate +--@field #number reloads --- -- @param #AMMOTRUCK self @@ -369,6 +373,7 @@ function AMMOTRUCK:CheckReturningTrucks(dataset) truck.statusquo = AMMOTRUCK.State.IDLE truck.timestamp = timer.getAbsTime() truck.coordinate = coord + truck.reloads = self.reloads or 5 self:__TruckHome(1,truck) end end @@ -540,6 +545,7 @@ function AMMOTRUCK:CheckTrucksAlive() newtruck.statusquo = AMMOTRUCK.State.IDLE newtruck.timestamp = timer.getAbsTime() newtruck.coordinate = truck:GetCoordinate() + newtruck.reloads = self.reloads or 5 self.trucklist[name] = newtruck end end @@ -626,8 +632,10 @@ function AMMOTRUCK:onafterMonitor(From, Event, To) unloadingtrucks[#unloadingtrucks+1] = data elseif data.statusquo == AMMOTRUCK.State.RETURNING then returningtrucks[#returningtrucks+1] = data + if data.reloads > 0 or data.reloads == -1 then idletrucks[#idletrucks+1] = data found = true + end end else self.truckset[data.name] = nil @@ -637,7 +645,7 @@ function AMMOTRUCK:onafterMonitor(From, Event, To) local n=0 if found and remunition then -- match - local match = false + --local match = false for _,_truckdata in pairs(idletrucks) do local truckdata = _truckdata -- #AMMOTRUCK.data local truckcoord = truckdata.group:GetCoordinate() -- Core.Point#COORDINATE @@ -750,6 +758,12 @@ end end local scheduler = SCHEDULER:New(nil,destroyammo,{ammo},self.waitingtime) + + -- one reload less + if truck.reloads ~= -1 then + truck.reloads = truck.reloads - 1 + end + return self end --- From 10872918bb7d0319a3d6ac3551498835b30ca44c Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 3 Jul 2023 17:29:06 +0200 Subject: [PATCH 12/25] #AUFTRAG * Added GROUNDESCORT from the DCS Task with same name --- Moose Development/Moose/Ops/Auftrag.lua | 60 ++++++++++++++++++++- Moose Development/Moose/Utilities/Enums.lua | 2 + 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index 00fb6ddbd..7d4deac86 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -1,3 +1,4 @@ +---@diagnostic disable: undefined-global --- **Ops** - Auftrag (mission) for Ops. -- -- ## Main Features: @@ -259,6 +260,10 @@ -- -- Not implemented yet. -- +-- ## Ground Escort +-- +-- An escort mission can be created with the @{#AUFTRAG.NewGROUNDESCORT}() function. +-- -- ## Intercept -- -- An intercept mission can be created with the @{#AUFTRAG.NewINTERCEPT}() function. @@ -406,6 +411,7 @@ _AUFTRAGSNR=0 -- @field #string FAC Forward AirController mission. -- @field #string FACA Forward AirController airborne mission. -- @field #string FERRY Ferry mission. +-- @field #string GROUNDESCORT Ground escort mission. -- @field #string INTERCEPT Intercept mission. -- @field #string ORBIT Orbit mission. -- @field #string GCICAP Similar to CAP but no auto engage targets. @@ -450,6 +456,7 @@ AUFTRAG.Type={ FAC="FAC", FACA="FAC-A", FERRY="Ferry Flight", + GROUNDESCORT="Ground Escort", INTERCEPT="Intercept", ORBIT="Orbit", GCICAP="Ground Controlled CAP", @@ -477,7 +484,7 @@ AUFTRAG.Type={ RELOCATECOHORT="Relocate Cohort", AIRDEFENSE="Air Defence", EWR="Early Warning Radar", - RECOVERYTANKER="Recovery Tanker", + --RECOVERYTANKER="Recovery Tanker", REARMING="Rearming", CAPTUREZONE="Capture Zone", NOTHING="Nothing", @@ -1740,6 +1747,43 @@ function AUFTRAG:NewBOMBCARPET(Target, Altitude, CarpetLength) return mission end +--- **[AIR/HELO]** Create a GROUNDESCORT (or FOLLOW) mission. Helo will escort a **ground** group and automatically engage certain target types. +-- @param #AUFTRAG self +-- @param Wrapper.Group#GROUP EscortGroup The ground group to escort. +-- @param #number OrbitDistance Orbit to/from the lead unit this many NM. Defaults to 1.5 NM. +-- @param #table TargetTypes Types of targets to engage automatically. Default is {"Ground vehicles"}, i.e. all enemy ground units. Use an empty set {} for a simple "FOLLOW" mission. +-- @return #AUFTRAG self +function AUFTRAG:NewGROUNDESCORT(EscortGroup, OrbitDistance, TargetTypes) + + local mission=AUFTRAG:New(AUFTRAG.Type.GROUNDESCORT) + + -- If only a string is passed we set a variable and check later if the group exists. + if type(EscortGroup)=="string" then + mission.escortGroupName=EscortGroup + mission:_TargetFromObject() + else + mission:_TargetFromObject(EscortGroup) + end + + -- DCS task parameters: + mission.orbitDistance=OrbitDistance and UTILS.NMToMeters(OrbitDistance) or UTILS.NMToMeters(1.5) + --mission.engageMaxDistance=EngageMaxDistance and UTILS.NMToMeters(EngageMaxDistance) or UTILS.NMToMeters(5) + mission.engageTargetTypes=TargetTypes or {"Ground vehicles"} + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.GROUNDESCORT + mission.missionFraction=0.1 + mission.missionAltitude=100 + mission.optionROE=ENUMS.ROE.OpenFire -- TODO: what's the best ROE here? Make dependent on ESCORT or FOLLOW! + mission.optionROT=ENUMS.ROT.EvadeFire + + mission.categories={AUFTRAG.Category.HELICOPTER} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + --- **[AIR]** Create an ESCORT (or FOLLOW) mission. Flight will escort another group and automatically engage certain target types. -- @param #AUFTRAG self @@ -5843,10 +5887,20 @@ function AUFTRAG:GetDCSMissionTask() -- ESCORT Mission -- -------------------- - local DCStask=CONTROLLABLE.TaskEscort(nil, self.engageTarget:GetObject(), self.escortVec3, LastWaypointIndex, self.engageMaxDistance, self.engageTargetTypes) + local DCStask=CONTROLLABLE.TaskEscort(nil, self.engageTarget:GetObject(), self.escortVec3, nil, self.engageMaxDistance, self.engageTargetTypes) table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.GROUNDESCORT then + -------------------- + -- GROUNDESCORT Mission -- + -------------------- + + local DCSTask=CONTROLLABLE.TaskGroundEscort(nil,self.engageTarget:GetObject(),nil,self.orbitDistance,self.engageTargetTypes) + + table.insert(DCStasks, DCSTask) + elseif self.type==AUFTRAG.Type.FACA then ------------------ @@ -6528,6 +6582,8 @@ function AUFTRAG:GetMissionTaskforMissionType(MissionType) mtask=ENUMS.MissionTask.AFAC elseif MissionType==AUFTRAG.Type.FERRY then mtask=ENUMS.MissionTask.NOTHING + elseif MissionType==AUFTRAG.Type.GROUNDESCORT then + mtask=ENUMS.MissionTask.GROUNDESCORT elseif MissionType==AUFTRAG.Type.INTERCEPT then mtask=ENUMS.MissionTask.INTERCEPT elseif MissionType==AUFTRAG.Type.RECON then diff --git a/Moose Development/Moose/Utilities/Enums.lua b/Moose Development/Moose/Utilities/Enums.lua index 6c47c5216..b00552dcf 100644 --- a/Moose Development/Moose/Utilities/Enums.lua +++ b/Moose Development/Moose/Utilities/Enums.lua @@ -236,6 +236,7 @@ ENUMS.WeaponType.Any={ -- @field #string ESCORT Escort another group. -- @field #string FIGHTERSWEEP Fighter sweep. -- @field #string GROUNDATTACK Ground attack. +-- @field #string GROUNDESCORT Ground escort another group. -- @field #string INTERCEPT Intercept. -- @field #string PINPOINTSTRIKE Pinpoint strike. -- @field #string RECONNAISSANCE Reconnaissance mission. @@ -251,6 +252,7 @@ ENUMS.MissionTask={ CAP="CAP", CAS="CAS", ESCORT="Escort", + GROUNDESCORT="Ground escort", FIGHTERSWEEP="Fighter Sweep", GROUNDATTACK="Ground Attack", INTERCEPT="Intercept", From 40fa929eb0d2a17b2a9db34970af31652f16b663 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Fri, 7 Jul 2023 15:14:33 +0200 Subject: [PATCH 13/25] * Added omissed DetectedItem.Name found by @Nocke --- Moose Development/Moose/Functional/Detection.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index e7f76ef0f..3558354a1 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -2354,6 +2354,7 @@ do -- DETECTION_TYPES if not DetectedItem then DetectedItem = self:AddDetectedItem( "TYPE", DetectedTypeName ) DetectedItem.TypeName = DetectedTypeName + DetectedItem.Name = DetectedUnitName -- fix by @Nocke end DetectedItem.Set:AddUnit( DetectedUnit ) From fb8f804af2abdefb5afd6482ec0970432d7a7350 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Tue, 11 Jul 2023 15:37:41 +0200 Subject: [PATCH 14/25] # CLIENTMENU * Initial release --- Moose Development/Moose/Core/ClientMenu.lua | 892 ++++++++++++++++++++ Moose Development/Moose/Modules.lua | 1 + Moose Setup/Moose.files | 1 + 3 files changed, 894 insertions(+) create mode 100644 Moose Development/Moose/Core/ClientMenu.lua diff --git a/Moose Development/Moose/Core/ClientMenu.lua b/Moose Development/Moose/Core/ClientMenu.lua new file mode 100644 index 000000000..3f43642e4 --- /dev/null +++ b/Moose Development/Moose/Core/ClientMenu.lua @@ -0,0 +1,892 @@ +--- **Core** - Client Menu Management. +-- +-- **Main Features:** +-- +-- * For complex, non-static menu structures +-- * Separation of menu tree creation from pushing it to clients +-- * Works with a SET_CLIENT set of clients +-- * Allow manipulation of the shadow tree in various ways +-- * Push to all or only one client +-- * Change entries' menu text, even if they have a sub-structure +-- * Option to make an entry usable once +-- +-- === +-- +-- ### Author: **applevangelist** +-- +-- === +-- @module Core.ClientMenu +-- @image Core_Menu.JPG +-- last change: July 2023 + +-- TODO +---------------------------------------------------------------------------------------------------------------- +-- +-- CLIENTMENU +-- +---------------------------------------------------------------------------------------------------------------- + +--- +-- @type CLIENTMENU +-- @field #string ClassName Class Name +-- @field #string lid Lid for log entries +-- @field #string version Version string +-- @field #string name Name +-- @field #table path +-- @field #table parentpath +-- @field #CLIENTMENU Parent +-- @field Wrapper.Client#CLIENT client +-- @field #number GID +-- @field #number ID +-- @field Wrapper.Group#GROUP group +-- @field #string Function +-- @field #table Functionargs +-- @field #table Children +-- @field #boolean Once +-- @field #boolean Generic +-- @field #boolean debug +-- @field #CLIENTMENUMANAGER Controller +-- @extends Core.Base#BASE + +--- +-- @field #CLIENTMENU +CLIENTMENU = { + ClassName = "CLIENTMENUE", + lid = "", + version = "0.0.1", + name = nil, + path = nil, + group = nil, + client = nil, + GID = nil, + Children = {}, + Once = false, + Generic = false, + debug = false, + Controller = nil, +} + +--- +-- @field #CLIENTMENU_ID +CLIENTMENU_ID = 0 + +--- Create an new CLIENTMENU object. +-- @param #CLIENTMENU self +-- @param Wrapper.Client#CLIENT Client The client for whom this entry is. +-- @param #string Text Text of the F10 menu entry. +-- @param #CLIENTMENU Parent The parent menu entry. +-- @param #string Function (optional) Function to call when the entry is used. +-- @param ... (optional) Arguments for the Function, comma separated +-- @return #CLIENTMENU self +function CLIENTMENU:NewEntry(Client,Text,Parent,Function,...) + -- Inherit everything from BASE class. + local self=BASE:Inherit(self, BASE:New()) -- #CLIENTMENU + CLIENTMENU_ID = CLIENTMENU_ID + 1 + self.ID = CLIENTMENU_ID + if Client then + self.group = Client:GetGroup() + self.client = Client + self.GID = self.group:GetID() + else + self.Generic = true + end + self.name = Text or "unknown entry" + if Parent then + self.parentpath = Parent:GetPath() + Parent:AddChild(self) + end + self.Parent = Parent + self.Function = Function + self.Functionargs = arg + if self.Functionargs and self.debug then + self:I({"Functionargs",self.Functionargs}) + end + if not self.Generic then + if Function ~= nil then + local ErrorHandler = function( errmsg ) + env.info( "MOOSE Error in CLIENTMENU COMMAND function: " .. errmsg ) + if BASE.Debug ~= nil then + env.info( BASE.Debug.traceback() ) + end + return errmsg + end + self.CallHandler = function() + local function MenuFunction() + return self.Function( unpack( self.Functionargs ) ) + end + local Status, Result = xpcall( MenuFunction, ErrorHandler) + if self.Once == true then + self:Clear() + end + end + self.path = missionCommands.addCommandForGroup(self.GID,Text,self.parentpath, self.CallHandler) + else + self.path = missionCommands.addSubMenuForGroup(self.GID,Text,self.parentpath) + end + else + if self.parentpath then + self.path = UTILS.DeepCopy(self.parentpath) + else + self.path = {} + end + self.path[#self.path+1] = Text + self:T({self.path}) + end + self.Once = false + -- Log id. + self.lid=string.format("CLIENTMENU %s | %s | ", self.ID, self.name) + self:T(self.lid.."Created") + return self +end + +--- Set the CLIENTMENUMANAGER for this entry. +-- @param #CLIENTMENU self +-- @param #CLIENTMENUMANAGER Controller The controlling object. +-- @return #CLIENTMENU self +function CLIENTMENU:SetController(Controller) + self.Controller = Controller + return self +end + +--- The entry will be deleted after being used used - for menu entries with functions only. +-- @param #CLIENTMENU self +-- @return #CLIENTMENU self +function CLIENTMENU:SetOnce() + self:T(self.lid.."SetOnce") + self.Once = true + return self +end + +--- Remove the entry from the F10 menu. +-- @param #CLIENTMENU self +-- @return #CLIENTMENU self +function CLIENTMENU:RemoveF10() + self:T(self.lid.."RemoveF10") + if not self.Generic then + missionCommands.removeItemForGroup(self.GID , self.path ) + end + return self +end + +--- Get the menu path table. +-- @param #CLIENTMENU self +-- @return #table Path +function CLIENTMENU:GetPath() + self:T(self.lid.."GetPath") + return self.path +end + +--- Link a child entry. +-- @param #CLIENTMENU self +-- @param #CLIENTMENU Child The entry to link as a child. +-- @return #CLIENTMENU self +function CLIENTMENU:AddChild(Child) + self:T(self.lid.."AddChild "..Child.ID) + table.insert(self.Children,Child.ID,Child) + return self +end + +--- Remove a child entry. +-- @param #CLIENTMENU self +-- @param #CLIENTMENU Child The entry to remove from the children. +-- @return #CLIENTMENU self +function CLIENTMENU:RemoveChild(Child) + self:T(self.lid.."RemoveChild "..Child.ID) + table.remove(self.Children,Child.ID) + return self +end + +--- Remove all subentries (children) from this entry. +-- @param #CLIENTMENU self +-- @return #CLIENTMENU self +function CLIENTMENU:RemoveSubEntries() + self:T(self.lid.."RemoveSubEntries") + --self:T({self.Children}) + for _id,_entry in pairs(self.Children) do + self:T("Removing ".._id) + if _entry then + _entry:RemoveSubEntries() + _entry:RemoveF10() + if _entry.Parent then + _entry.Parent:RemoveChild(self) + end + if self.Controller then + self.Controller:_RemoveByID(_entry.ID) + end + _entry = nil + end + end + return self +end + +--- Remove this entry and all subentries (children) from this entry. +-- @param #CLIENTMENU self +-- @return #CLIENTMENU self +function CLIENTMENU:Clear() + self:T(self.lid.."Clear") + for _id,_entry in pairs(self.Children) do + if _entry then + _entry:RemoveSubEntries() + _entry = nil + end + end + self:RemoveF10() + if self.Parent then + self.Parent:RemoveChild(self) + end + if self.Controller then + self.Controller:_RemoveByID(self.ID) + end + return self +end + +-- TODO +---------------------------------------------------------------------------------------------------------------- +-- +-- CLIENTMENUMANAGER +-- +---------------------------------------------------------------------------------------------------------------- + +---CLIENTMENUMANAGER class +-- @type CLIENTMENUMANAGER +-- @field #string ClassName Class Name +-- @field #string lid Lid for log entries +-- @field #string version Version string +-- @field #string name Name +-- @field Core.Set#SET_CLIENT clientset The set of clients this menu manager is for +-- @field #table structure +-- @field #table replacementstructure +-- @field #table rootentries +-- @field #number entrycount +-- @field #boolean debug +-- @extends Core.Base#BASE + +--- *As a child my family's menu consisted of two choices: take it, or leave it.* +-- +-- === +-- +-- # CLIENTMENU and CLIENTMENUMANAGER +-- +-- Manage menu structures for a SET_CLIENT of clients. +-- +-- ## Concept +-- +-- Separate creation of a menu tree structure from pushing it to each client. Create a shadow "reference" menu structure tree for your client pilot's in a mission. +-- This can then be propagated to all clients. Manipulate the entries in the structure with removing, clearing or changing single entries, create replacement sub-structures +-- for entries etc, push to one or all clients. +-- +-- Many functions can either change the tree for one client or for all clients. +-- +-- ## Create a base reference tree and send to all clients +-- +-- local clientset = SET_CLIENT:New():FilterStart() +-- +-- local menumgr = CLIENTMENUMANAGER:New(clientset,"Dayshift") +-- local mymenu = menumgr:NewEntry("Top") +-- local mymenu_lv1a = menumgr:NewEntry("Level 1 a",mymenu) +-- local mymenu_lv1b = menumgr:NewEntry("Level 1 b",mymenu) +-- -- next one is a command menu entry, which can only be used once +-- local mymenu_lv1c = menumgr:NewEntry("Action Level 1 c",mymenu, testfunction, "testtext"):SetOnce() +-- +-- local mymenu_lv2a = menumgr:NewEntry("Go here",mymenu_lv1a) +-- local mymenu_lv2b = menumgr:NewEntry("Level 2 ab",mymenu_lv1a) +-- local mymenu_lv2c = menumgr:NewEntry("Level 2 ac",mymenu_lv1a) +-- +-- local mymenu_lv2ba = menumgr:NewEntry("Level 2 ba",mymenu_lv1b) +-- local mymenu_lv2bb = menumgr:NewEntry("Level 2 bb",mymenu_lv1b) +-- local mymenu_lv2bc = menumgr:NewEntry("Level 2 bc",mymenu_lv1b) +-- +-- local mymenu_lv3a = menumgr:NewEntry("Level 3 aaa",mymenu_lv2a) +-- local mymenu_lv3b = menumgr:NewEntry("Level 3 aab",mymenu_lv2a) +-- local mymenu_lv3c = menumgr:NewEntry("Level 3 aac",mymenu_lv2a) +-- +-- menumgr:Propagate() +-- +-- ## Remove a single entry's subtree +-- +-- menumgr:RemoveSubEntries(mymenu_lv3a) +-- + -- ## Remove a single entry and also it's subtree +-- +-- menumgr:Clear(mymenu_lv3a) +-- +-- ## Add a single entry +-- +-- local baimenu = menumgr:NewEntry("BAI",mymenu_lv1b) +-- menumgr:AddEntry(baimenu) +-- +-- ## Prepare and push a partial replacement in the tree +-- +-- menumgr:PrepareNewReplacementStructure() +-- local submenu = menumgr:NewReplacementEntry("New Level 2 ba",mymenu_lv2a) +-- menumgr:NewReplacementEntry("New Level 2 bb",mymenu_lv2a) +-- menumgr:NewReplacementEntry("Deleted",mymenu_lv2a) +-- menumgr:NewReplacementEntry("New Level 2 bd",mymenu_lv2a) +-- menumgr:NewReplacementEntry("SubLevel 3 baa",submenu) +-- menumgr:NewReplacementEntry("SubLevel 3 bab",submenu) +-- menumgr:NewReplacementEntry("SubLevel 3 bac",submenu) +-- menumgr:NewReplacementEntry("SubLevel 3 bad",submenu) +-- menumgr:ReplaceEntries(mymenu_lv2a) +-- +-- ## Change the text of an entry in the menu tree +-- +-- menumgr:ChangeEntryTextForAll(mymenu_lv1b,"Attack") +-- +-- ## Reset a single clients menu tree +-- +-- menumgr:ResetMenu(client) +-- +-- ## Reset all and clear the reference tree +-- +-- menumgr:ResetMenuComplete() +--- +-- @type CLIENTMENUMANAGER.Structure +-- @field #table generic +-- @field #table IDs + +--- +-- @field #CLIENTMENUMANAGER +CLIENTMENUMANAGER = { + ClassName = "CLIENTMENUMANAGER", + lid = "", + version = "0.0.1", + name = nil, + clientset = nil, + --- + -- @field #CLIENTMENUMANAGER.Structure + structure = { + generic = {}, + IDs = {}, + }, + --- + -- #CLIENTMENUMANAGER.ReplacementStructure + replacementstructure = { + generic = {}, + IDs = {}, + }, + entrycount = 0, + rootentries = {}, + debug = true, +} + + +--- Create a new ClientManager instance. +-- @param #CLIENTMENUMANAGER self +-- @param Core.Set#SET_CLIENT ClientSet The set of clients to manage. +-- @param #string Alias The name of this manager. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:New(ClientSet, Alias) + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, BASE:New()) -- #CLIENTMENUMANAGER + self.clientset = ClientSet + self.name = Alias or "Nightshift" + -- Log id. + self.lid=string.format("CLIENTMENUMANAGER %s | %s | ", self.version, self.name) + if self.debug then + self:I(self.lid.."Created") + end + return self +end + +--- Create a new entry in the generic structure. +-- @param #CLIENTMENUMANAGER self +-- @param #string Text Text of the F10 menu entry. +-- @param #CLIENTMENU Parent The parent menu entry. +-- @param #string Function (optional) Function to call when the entry is used. +-- @param ... (optional) Arguments for the Function, comma separated. +-- @return #CLIENTMENU Entry +function CLIENTMENUMANAGER:NewEntry(Text,Parent,Function,...) + self:T(self.lid.."NewEntry "..Text or "None") + self.entrycount = self.entrycount + 1 + local entry = CLIENTMENU:NewEntry(nil,Text,Parent,Function,unpack(arg)) + self.structure.generic[self.entrycount] = entry + self.structure.IDs[entry.ID] = self.entrycount + if not Parent then + self.rootentries[self.entrycount] = self.entrycount + end + return entry +end + +--- Find **first** matching entry in the generic structure by the menu text. +-- @param #CLIENTMENUMANAGER self +-- @param #string Text Text of the F10 menu entry. +-- @return #CLIENTMENU Entry The #CLIENTMENU object found or nil. +-- @return #number GID GID The GID found or nil. +function CLIENTMENUMANAGER:FindEntryByText(Text) + self:T(self.lid.."FindEntryByText "..Text or "None") + local entry = nil + local gid = nil + for _gid,_entry in UTILS.spairs(self.structure.generic) do + local Entry = _entry -- #CLIENTMENU + if Entry and Entry.name == Text then + entry = Entry + gid = _gid + end + end + return entry, gid +end + +--- Find first matching entry in the generic structure by the GID. +-- @param #CLIENTMENUMANAGER self +-- @param #number GID The GID of the entry to find. +-- @return #CLIENTMENU Entry The #CLIENTMENU object found or nil. +function CLIENTMENUMANAGER:GetEntryByGID(GID) + self:T(self.lid.."GetEntryByGID "..GID or "None") + if GID and type(GID) == "number" then + return self.structure.generic[GID] + else + return nil + end +end + +--- Alter the text of an entry in the generic structure and push to all clients. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The menu entry. +-- @param #string Text Text of the F10 menu entry. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:ChangeEntryTextForAll(Entry,Text) + self:T(self.lid.."ChangeEntryTextForAll "..Text or "None") + for _,_client in pairs(self.clientset.Set) do + local client = _client -- Wrapper.Client#CLIENT + if client and client:IsAlive() then + self:ChangeEntryText(Entry,Text, client) + end + end + return self +end + +--- Alter the text of an entry in the generic structure and push to one specific client. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The menu entry. +-- @param #string Text Text of the F10 menu entry. +-- @param Wrapper.Client#CLIENT Client The client for whom to alter the entry +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:ChangeEntryText(Entry,Text, Client) + self:T(self.lid.."ChangeEntryText "..Text or "None") + + local text = Text or "none" + local oldtext = Entry.name + Entry.name = text + + local newstructure = {} + local changed = 0 + + local function ChangePath(path,oldtext,newtext) + local newpath = {} + for _id,_text in UTILS.spairs(path) do + local txt = _text + if _text == oldtext then + txt = newtext + end + newpath[_id] = txt + end + return newpath + end + + local function AlterPath(children) + for _,_entry in pairs(children) do + local entry = _entry -- #CLIENTMENU + local newpath = ChangePath(entry.path,oldtext,text) + local newparentpath = ChangePath(entry.parentpath,oldtext,text) + entry.path = nil + entry.parentpath = nil + entry.path = newpath + entry.parentpath = newparentpath + self:T({entry.ID}) + --self:T({entry.parentpath}) + newstructure[entry.ID] = UTILS.DeepCopy(entry) + changed = changed + 1 + if entry.Children and #entry.Children > 0 then + AlterPath(entry.Children) + end + end + end + + -- get the entry + local ID = Entry.ID + local GID = self.structure.IDs[ID] + local playername = Client:GetPlayerName() + local children = self.structure[playername][GID].Children + AlterPath(children) + + self:T("Changed entries: "..changed) + + local NewParent = self:NewEntry(Entry.name,Entry.Parent,Entry.Function,unpack(Entry.Functionargs)) + + for _,_entry in pairs(children) do + self:T("Changed parent for ".._entry.ID.." | GID ".._entry.GID) + local entry = _entry -- #CLIENTMENU + entry.Parent = NewParent + end + + self:PrepareNewReplacementStructure() + + for _,_entry in pairs(newstructure) do + self:T("Changed entry: ".._entry.ID.." | GID ".._entry.GID) + local entry = _entry -- #CLIENTMENU + self:NewReplacementEntry(entry.name,entry.Parent,entry.Function,unpack(entry.Functionargs)) + end + + + self:AddEntry(NewParent) + self:ReplaceEntries(NewParent) + + self:Clear(Entry) + + return self +end + +--- Create a new entry in the replacement structure. +-- @param #CLIENTMENUMANAGER self +-- @param #string Text Text of the F10 menu entry. +-- @param #CLIENTMENU Parent The parent menu entry. +-- @param #string Function (optional) Function to call when the entry is used. +-- @param ... (optional) Arguments for the Function, comma separated +-- @return #CLIENTMENU Entry +function CLIENTMENUMANAGER:NewReplacementEntry(Text,Parent,Function,...) + self:T(self.lid.."NewReplacementEntry "..Text or "None") + self.entrycount = self.entrycount + 1 + local entry = CLIENTMENU:NewEntry(nil,Text,Parent,Function,unpack(arg)) + self.replacementstructure.generic[self.entrycount] = entry + self.replacementstructure.IDs[entry.ID] = self.entrycount + local pID = Parent and Parent.ID or "none" + if self.debug then + self:I("Entry ID = "..self.entrycount.." | Parent ID = "..tostring(pID)) + end + if not Parent then + self.rootentries[self.entrycount] = self.entrycount + end + return entry +end + +--- Prepare a new replacement structure. Deletes the previous one. +-- @param #CLIENTMENUMANAGER self +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:PrepareNewReplacementStructure() + self:T(self.lid.."PrepareNewReplacementStructure") + self.replacementstructure = nil -- #CLIENTMENUMANAGER.Structure + self.replacementstructure = { + generic = {}, + IDs = {}, + } + return self +end + +--- [Internal] Merge the replacement structure into the generic structure. +-- @param #CLIENTMENUMANAGER self +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:_MergeReplacementData() + self:T(self.lid.."_MergeReplacementData") + for _id,_entry in pairs(self.replacementstructure.generic) do + self.structure.generic[_id] = _entry + end + for _id,_entry in pairs(self.replacementstructure.IDs) do + self.structure.IDs[_id] = _entry + end + self:_CleanUpPlayerStructure() + return self +end + +--- Replace entries under the Parent entry with the Replacement structure created prior for all clients. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Parent The parent entry under which to replace with the new structure. +-- @param Wrapper.Client#CLIENT Client (optional) If given, make this change only for this client. In this case the generic structure will not be touched. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:ReplaceEntries(Parent,Client) + self:T(self.lid.."ReplaceEntries") + -- clear Parent substructure + local Set = self.clientset.Set + if Client then + Set = {Client} + else + self:RemoveSubEntries(Parent) + end + for _,_client in pairs(Set) do + local client = _client -- Wrapper.Client#CLIENT + if client and client:IsAlive() then + local playername = client:GetPlayerName() + --self.structure[playername] = {} + for _id,_entry in UTILS.spairs(self.replacementstructure.generic) do + local entry = _entry -- #CLIENTMENU + local parent = Parent + self:T("Posted Parent = "..Parent.ID) + if entry.Parent and entry.Parent.name then + parent = self:_GetParentEntry(self.replacementstructure.generic,entry.Parent.name) or Parent + self:T("Found Parent = "..parent.ID) + end + self.structure[playername][_id] = CLIENTMENU:NewEntry(client,entry.name,parent,entry.Function,unpack(entry.Functionargs)) + self.structure[playername][_id].Once = entry.Once + end + end + end + self:_MergeReplacementData() + return self +end + +--- [Internal] Find a parent entry in a given structure by name. +-- @param #CLIENTMENUMANAGER self +-- @param #table Structure Table of entries. +-- @param #string Name Name to find. +-- @return #CLIENTMENU Entry +function CLIENTMENUMANAGER:_GetParentEntry(Structure,Name) + self:T(self.lid.."_GetParentEntry") + local found = nil + for _,_entry in pairs(Structure) do + local entry = _entry -- #CLIENTMENU + if entry.name == Name then + found = entry + break + end + end + return found +end + +--- Push the complete menu structure to each of the clients in the set. +-- @param #CLIENTMENUMANAGER self +-- @param Wrapper.Client#CLIENT Client (optional) If given, propagate only for this client. +-- @return #CLIENTMENU Entry +function CLIENTMENUMANAGER:Propagate(Client) + self:T(self.lid.."Propagate") + local Set = self.clientset.Set + if Client then + Set = {Set} + end + for _,_client in pairs(Set) do + local client = _client -- Wrapper.Client#CLIENT + if client and client:IsAlive() then + local playername = client:GetPlayerName() + self.structure[playername] = {} + for _id,_entry in pairs(self.structure.generic) do + local entry = _entry -- #CLIENTMENU + local parent = nil + if entry.Parent and entry.Parent.name then + parent = self:_GetParentEntry(self.structure[playername],entry.Parent.name) + end + self.structure[playername][_id] = CLIENTMENU:NewEntry(client,entry.name,parent,entry.Function,unpack(entry.Functionargs)) + self.structure[playername][_id].Once = entry.Once + end + end + end + return self +end + +--- Push a single previously created entry into the menu structure of all clients. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The entry to add. +-- @param Wrapper.Client#CLIENT Client (optional) If given, make this change only for this client. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:AddEntry(Entry,Client) + self:T(self.lid.."AddEntry") + local Set = self.clientset.Set + if Client then + Set = {Client} + end + for _,_client in pairs(Set) do + local client = _client -- Wrapper.Client#CLIENT + if client and client:IsAlive() then + local playername = client:GetPlayerName() + local entry = Entry -- #CLIENTMENU + local parent = nil + if entry.Parent and entry.Parent.name then + parent = self:_GetParentEntry(self.structure[playername],entry.Parent.name) + end + self.structure[playername][Entry.ID] = CLIENTMENU:NewEntry(client,entry.name,parent,entry.Function,unpack(entry.Functionargs)) + self.structure[playername][Entry.ID].Once = entry.Once + end + end + return self +end + +--- Blank out the menu - remove **all root entries** and all entries below from the client's menus, leaving the generic structure untouched. +-- @param #CLIENTMENUMANAGER self +-- @param Wrapper.Client#CLIENT Client +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:ResetMenu(Client) + self:T(self.lid.."ResetMenu") + for _,_entry in pairs(self.rootentries) do + local RootEntry = self.structure.generic[_entry] + if RootEntry then + self:Clear(RootEntry,Client) + end + end + return self +end + +--- Blank out the menu - remove **all root entries** and all entries below from all clients' menus, and **delete** the generic structure. +-- @param #CLIENTMENUMANAGER self +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:ResetMenuComplete() + self:T(self.lid.."ResetMenuComplete") + for _,_entry in pairs(self.rootentries) do + local RootEntry = self.structure.generic[_entry] + if RootEntry then + self:Clear(RootEntry) + end + end + self.structure = nil + self.structure = { + generic = {}, + IDs = {}, + } + self.rootentries = nil + self.rootentries = {} + return self +end + +--- Remove the entry and all entries below the given entry from the client's menus and the generic structure. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The entry to remove +-- @param Wrapper.Client#CLIENT Client (optional) If given, make this change only for this client. In this case the generic structure will not be touched. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:Clear(Entry,Client) + self:T(self.lid.."Clear") + local rid = self.structure.IDs[Entry.ID] + if rid then + local generic = self.structure.generic[rid] + local Set = self.clientset.Set + if Client then + Set = {Client} + end + for _,_client in pairs(Set) do + local client = _client -- Wrapper.Client#CLIENT + if client and client:IsAlive() then + local playername = client:GetPlayerName() + local entry = self.structure[playername][rid] -- #CLIENTMENU + if entry then + entry:Clear() + self.structure[playername][rid] = nil + end + end + end + if not Client then + for _id,_entry in pairs(self.structure.generic) do + local entry = _entry -- #CLIENTMENU + if entry and entry.Parent and entry.Parent.ID and entry.Parent.ID == rid then + self.structure.IDs[entry.ID] = nil + entry = nil + end + end + end + end + return self +end + +--- [Internal] Clean up player shadow structure +-- @param #CLIENTMENUMANAGER self +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:_CleanUpPlayerStructure() + self:T(self.lid.."_CleanUpPlayerStructure") + for _,_client in pairs(self.clientset.Set) do + local client = _client -- Wrapper.Client#CLIENT + if client and client:IsAlive() then + local playername = client:GetPlayerName() + local newstructure = {} + for _id, _entry in UTILS.spairs(self.structure[playername]) do + if self.structure.generic[_id] then + newstructure[_id] = _entry + end + end + self.structure[playername] = nil + self.structure[playername] = newstructure + end + end + return self +end + +--- Remove all entries below the given entry from the clients' menus and the generic structure. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The menu entry +-- @param Wrapper.Client#CLIENT Client (optional) If given, make this change only for this client. In this case the generic structure will not be touched. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:RemoveSubEntries(Entry,Client) + self:T(self.lid.."RemoveSubEntries") + local rid = self.structure.IDs[Entry.ID] + if rid then + local Set = self.clientset.Set + if Client then + Set = {Client} + end + for _,_client in pairs(Set) do + local client = _client -- Wrapper.Client#CLIENT + if client and client:IsAlive() then + local playername = client:GetPlayerName() + local entry = self.structure[playername][rid] -- #CLIENTMENU + if entry then + entry:RemoveSubEntries() + end + end + end + if not Client then + for _id,_entry in pairs(self.structure.generic) do + local entry = _entry -- #CLIENTMENU + if entry and entry.Parent and entry.Parent.ID and entry.Parent.ID == rid then + self.structure.IDs[entry.ID] = nil + self.structure.generic[_id] = nil + end + end + end + end + self:_CleanUpPlayerStructure() + return self +end + +--- Remove a specific entry by ID from the generic structure +-- @param #CLIENTMENUMANAGER self +-- @param #number ID +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:_RemoveByID(ID) + self:T(self.lid.."_RemoveByID "..ID or "none") + if ID then + local gid = self.structure.IDs[ID] + if gid then + self.structure.generic[gid] = nil + self.structure.IDs[ID] = nil + end + end + return self +end + +--- [Internal] Dump structures to log for debug +-- @param #CLIENTMENUMANAGER self +-- @param #string Playername +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:_CheckStructures(Playername) + self:T(self.lid.."CheckStructures") + self:I("Generic Structure") + self:I("-----------------") + for _id,_entry in UTILS.spairs(self.structure.generic) do + local ID = "none" + if _entry and _entry.ID then + ID = _entry.ID + end + self:I("ID= ".._id.." | EntryID = "..ID) + if _id > 10 and _id < 14 then + self:I(_entry.name) + end + end + self:I("Reverse Structure") + self:I("-----------------") + for _id,_entry in UTILS.spairs(self.structure.IDs) do + self:I("EntryID= ".._id.." | ID = ".._entry) + end + if Playername then + self:I("Player Structure") + self:I("-----------------") + for _id,_entry in UTILS.spairs(self.structure[Playername]) do + local ID = "none" + if _entry and _entry.ID then + ID = _entry.ID + end + local _lid = _id or "none" + self:I("ID= ".._lid.." | EntryID = "..ID) + end + end + return self +end + +---------------------------------------------------------------------------------------------------------------- +-- +-- End ClientMenu +-- +---------------------------------------------------------------------------------------------------------------- + diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 3d9f8f981..d5cad1818 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -12,6 +12,7 @@ __Moose.Include( 'Scripts/Moose/Core/Base.lua' ) __Moose.Include( 'Scripts/Moose/Core/Astar.lua' ) __Moose.Include( 'Scripts/Moose/Core/Beacon.lua' ) __Moose.Include( 'Scripts/Moose/Core/Condition.lua' ) +__Moose.Include( 'Scripts/Moose/Core/ClientMenu.lua') __Moose.Include( 'Scripts/Moose/Core/Database.lua' ) __Moose.Include( 'Scripts/Moose/Core/Event.lua' ) __Moose.Include( 'Scripts/Moose/Core/Fsm.lua' ) diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 6839b895e..0ad57277f 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -32,6 +32,7 @@ Core/Spot.lua Core/TextAndSound.lua Core/Condition.lua Core/Pathline.lua +Core/ClientMenu.lua Wrapper/Object.lua Wrapper/Identifiable.lua From 63ba50a83a7094e66a143175efa09e9e5ee61ff7 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Tue, 11 Jul 2023 16:10:40 +0200 Subject: [PATCH 15/25] #CLIENTMENUMANAGER Docu --- Moose Development/Moose/Core/ClientMenu.lua | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Moose Development/Moose/Core/ClientMenu.lua b/Moose Development/Moose/Core/ClientMenu.lua index 3f43642e4..60a74fe11 100644 --- a/Moose Development/Moose/Core/ClientMenu.lua +++ b/Moose Development/Moose/Core/ClientMenu.lua @@ -15,6 +15,7 @@ -- ### Author: **applevangelist** -- -- === +-- -- @module Core.ClientMenu -- @image Core_Menu.JPG -- last change: July 2023 @@ -247,7 +248,8 @@ end -- ---------------------------------------------------------------------------------------------------------------- ----CLIENTMENUMANAGER class + +--- Class CLIENTMENUMANAGER -- @type CLIENTMENUMANAGER -- @field #string ClassName Class Name -- @field #string lid Lid for log entries @@ -265,7 +267,7 @@ end -- -- === -- --- # CLIENTMENU and CLIENTMENUMANAGER +-- ## CLIENTMENU and CLIENTMENUMANAGER -- -- Manage menu structures for a SET_CLIENT of clients. -- @@ -338,13 +340,8 @@ end -- -- ## Reset all and clear the reference tree -- --- menumgr:ResetMenuComplete() ---- --- @type CLIENTMENUMANAGER.Structure --- @field #table generic --- @field #table IDs - ---- +-- menumgr:ResetMenuComplete() +-- -- @field #CLIENTMENUMANAGER CLIENTMENUMANAGER = { ClassName = "CLIENTMENUMANAGER", @@ -369,6 +366,10 @@ CLIENTMENUMANAGER = { debug = true, } +--- +-- @type CLIENTMENUMANAGER.Structure +-- @field #table generic +-- @field #table IDs --- Create a new ClientManager instance. -- @param #CLIENTMENUMANAGER self @@ -889,4 +890,3 @@ end -- End ClientMenu -- ---------------------------------------------------------------------------------------------------------------- - From 10b9a32f298e055033d84e39911019093ae14744 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Wed, 12 Jul 2023 17:54:17 +0200 Subject: [PATCH 16/25] #SPOT * Fix for switching laser off and on again not follwing unit any more --- Moose Development/Moose/Core/Spot.lua | 71 ++++++++++++++++----------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/Moose Development/Moose/Core/Spot.lua b/Moose Development/Moose/Core/Spot.lua index d87606937..9a12fdf35 100644 --- a/Moose Development/Moose/Core/Spot.lua +++ b/Moose Development/Moose/Core/Spot.lua @@ -33,7 +33,8 @@ do - --- @type SPOT + --- + -- @type SPOT -- @extends Core.Fsm#FSM @@ -228,7 +229,8 @@ do -- @param #number LaserCode Laser code. -- @param #number Duration Duration of lasing in seconds. function SPOT:onafterLaseOn( From, Event, To, Target, LaserCode, Duration ) - self:F( { "LaseOn", Target, LaserCode, Duration } ) + self:T({From, Event, To}) + self:T2( { "LaseOn", Target, LaserCode, Duration } ) local function StopLase( self ) self:LaseOff() @@ -256,6 +258,8 @@ do self:HandleEvent( EVENTS.Dead ) self:__Lasing( -1 ) + + return self end @@ -268,7 +272,7 @@ do -- @param #number LaserCode Laser code. -- @param #number Duration Duration of lasing in seconds. function SPOT:onafterLaseOnCoordinate(From, Event, To, Coordinate, LaserCode, Duration) - self:F( { "LaseOnCoordinate", Coordinate, LaserCode, Duration } ) + self:T2( { "LaseOnCoordinate", Coordinate, LaserCode, Duration } ) local function StopLase( self ) self:LaseOff() @@ -290,12 +294,14 @@ do end self:__Lasing(-1) + return self end - - --- @param #SPOT self + + --- + -- @param #SPOT self -- @param Core.Event#EVENTDATA EventData function SPOT:OnEventDead(EventData) - self:F( { Dead = EventData.IniDCSUnitName, Target = self.Target } ) + self:T2( { Dead = EventData.IniDCSUnitName, Target = self.Target } ) if self.Target then if EventData.IniDCSUnitName == self.TargetName then self:F( {"Target dead ", self.TargetName } ) @@ -309,42 +315,51 @@ do self:LaseOff() end end + return self end - --- @param #SPOT self + --- + -- @param #SPOT self -- @param From -- @param Event -- @param To function SPOT:onafterLasing( From, Event, To ) - - if self.Target and self.Target:IsAlive() then - self.SpotIR:setPoint( self.Target:GetPointVec3():AddY(1):AddY(math.random(-100,100)/100):AddX(math.random(-100,100)/100):GetVec3() ) - self.SpotLaser:setPoint( self.Target:GetPointVec3():AddY(1):GetVec3() ) - self:__Lasing( -0.2 ) - elseif self.TargetCoord then + self:T({From, Event, To}) - -- Wiggle the IR spot a bit. - local irvec3={x=self.TargetCoord.x+math.random(-100,100)/100, y=self.TargetCoord.y+math.random(-100,100)/100, z=self.TargetCoord.z} --#DCS.Vec3 - local lsvec3={x=self.TargetCoord.x, y=self.TargetCoord.y, z=self.TargetCoord.z} --#DCS.Vec3 + if self.Lasing then + if self.Target and self.Target:IsAlive() then + + self.SpotIR:setPoint( self.Target:GetPointVec3():AddY(1):AddY(math.random(-100,100)/100):AddX(math.random(-100,100)/100):GetVec3() ) + self.SpotLaser:setPoint( self.Target:GetPointVec3():AddY(1):GetVec3() ) + + self:__Lasing(0.2) + elseif self.TargetCoord then - self.SpotIR:setPoint(irvec3) - self.SpotLaser:setPoint(lsvec3) - - self:__Lasing(-0.25) - else - self:F( { "Target is not alive", self.Target:IsAlive() } ) + -- Wiggle the IR spot a bit. + local irvec3={x=self.TargetCoord.x+math.random(-100,100)/100, y=self.TargetCoord.y+math.random(-100,100)/100, z=self.TargetCoord.z} --#DCS.Vec3 + local lsvec3={x=self.TargetCoord.x, y=self.TargetCoord.y, z=self.TargetCoord.z} --#DCS.Vec3 + + self.SpotIR:setPoint(irvec3) + self.SpotLaser:setPoint(lsvec3) + + self:__Lasing(0.2) + else + self:F( { "Target is not alive", self.Target:IsAlive() } ) + end end - + return self end - - --- @param #SPOT self + + --- + -- @param #SPOT self -- @param From -- @param Event -- @param To -- @return #SPOT function SPOT:onafterLaseOff( From, Event, To ) - - self:F( {"Stopped lasing for ", self.Target and self.Target:GetName() or "coord", SpotIR = self.SportIR, SpotLaser = self.SpotLaser } ) + self:t({From, Event, To}) + + self:T2( {"Stopped lasing for ", self.Target and self.Target:GetName() or "coord", SpotIR = self.SportIR, SpotLaser = self.SpotLaser } ) self.Lasing = false @@ -383,4 +398,4 @@ do return self end -end \ No newline at end of file +end From 04a9dc3a8c14cc1dbfa0de1328c3f7d32e50a272 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 13 Jul 2023 16:13:00 +0200 Subject: [PATCH 17/25] #SPAWN * Added method to init spawn position --- Moose Development/Moose/Core/Spawn.lua | 113 +++++++++++++++++++++++-- 1 file changed, 104 insertions(+), 9 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index df22111c3..95719aa9e 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -162,7 +162,17 @@ -- ### Array formation -- -- * @{#SPAWN.InitArray}(): Make groups visible before they are actually activated, and order these groups like a battalion in an array. --- +-- +-- ### Group initial position - if wanted different from template position, for use with e.g. @{#SPAWN.SpawnScheduled}(). +-- +-- * @{#SPAWN.InitPositionCoordinate}(): Set initial position of group via a COORDINATE. +-- * @{#SPAWN.InitPositionVec2}(): Set initial position of group via a VEC2. +-- +-- ### Set the positions of a group's units to absolute positions, or relative positions to unit No. 1 +-- +-- * @{#SPAWN.InitSetUnitRelativePositions}(): Spawn the UNITs of this group with individual relative positions to unit #1 and individual headings. +-- * @{#SPAWN.InitSetUnitAbsolutePositions}(): Spawn the UNITs of this group with individual absolute positions and individual headings. +-- -- ### Position randomization -- -- * @{#SPAWN.InitRandomizePosition}(): Randomizes the position of @{Wrapper.Group}s that are spawned within a **radius band**, given an Outer and Inner radius, from the point that the spawn happens. @@ -268,7 +278,7 @@ SPAWN = { -- @type SPAWN.Takeoff -- @extends Wrapper.Group#GROUP.Takeoff ---- @field #SPAWN.Takeoff Takeoff +-- @field #SPAWN.Takeoff Takeoff SPAWN.Takeoff = { Air = 1, Runway = 2, @@ -276,7 +286,7 @@ SPAWN.Takeoff = { Cold = 4, } ---- @type SPAWN.SpawnZoneTable +-- @type SPAWN.SpawnZoneTable -- @list SpawnZone --- Creates the main object to spawn a @{Wrapper.Group} defined in the DCS ME. @@ -1047,7 +1057,7 @@ end --- This method provides the functionality to randomize the spawning of the Groups at a given list of zones of different types. -- @param #SPAWN self -- @param #table SpawnZoneTable A table with @{Core.Zone} objects. If this table is given, then each spawn will be executed within the given list of @{Core.Zone}s objects. --- @return #SPAWN +-- @return #SPAWN self -- @usage -- -- -- Create a zone table of the 2 zones. @@ -1077,6 +1087,31 @@ function SPAWN:InitRandomizeZones( SpawnZoneTable ) return self end +--- This method sets a spawn position for the group that is different from the location of the template. +-- @param #SPAWN self +-- @param Core.Point#COORDINATE Coordinate The position to spawn from +-- @return #SPAWN self +function SPAWN:InitPositionCoordinate(Coordinate) + self:T( { self.SpawnTemplatePrefix, Coordinate:GetVec2()} ) + self:InitPositionVec2(Coordinate:GetVec2()) + return self +end + +--- This method sets a spawn position for the group that is different from the location of the template. +-- @param #SPAWN self +-- @param DCS#Vec2 Vec2 The position to spawn from +-- @return #SPAWN self +function SPAWN:InitPositionVec2(Vec2) + self:T( { self.SpawnTemplatePrefix, Vec2} ) + self.SpawnInitPosition = Vec2 + self.SpawnFromNewPosition = true + self:I("MaxGroups:"..self.SpawnMaxGroups) + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:_SetInitialPosition( SpawnGroupID ) + end + return self +end + --- For planes and helicopters, when these groups go home and land on their home airbases and FARPs, they normally would taxi to the parking spot, shut-down their engines and wait forever until the Group is removed by the runtime environment. -- This method is used to re-spawn automatically (so no extra call is needed anymore) the same group after it has landed. -- This will enable a spawned group to be re-spawned after it lands, until it is destroyed... @@ -1376,7 +1411,12 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) self:F2( { SpawnTemplatePrefix = self.SpawnTemplatePrefix, SpawnIndex = SpawnIndex, AliveUnits = self.AliveUnits, SpawnMaxGroups = self.SpawnMaxGroups } ) if self:_GetSpawnIndex( SpawnIndex ) then - + + if self.SpawnFromNewPosition then + self:_SetInitialPosition( SpawnIndex ) + end + + if self.SpawnGroups[self.SpawnIndex].Visible then self.SpawnGroups[self.SpawnIndex].Group:Activate() else @@ -1614,7 +1654,7 @@ end -- @param #number SpawnTime The time interval defined in seconds between each new spawn of new groups. -- @param #number SpawnTimeVariation The variation to be applied on the defined time interval between each new spawn. -- The variation is a number between 0 and 1, representing the % of variation to be applied on the time interval. --- @param #boolen WithDelay Do not spawn the **first** group immediately, but delay the spawn as per the calculation below. +-- @param #boolean WithDelay Do not spawn the **first** group immediately, but delay the spawn as per the calculation below. -- Effectively the same as @{InitDelayOn}(). -- @return #SPAWN self -- @usage @@ -3127,7 +3167,11 @@ function SPAWN:_GetTemplate( SpawnTemplatePrefix ) self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnTemplatePrefix } ) local SpawnTemplate = nil - + + if _DATABASE.Templates.Groups[SpawnTemplatePrefix] == nil then + error( 'No Template exists for SpawnTemplatePrefix = ' .. SpawnTemplatePrefix ) + end + local Template = _DATABASE.Templates.Groups[SpawnTemplatePrefix].Template self:F( { Template = Template } ) @@ -3296,6 +3340,57 @@ function SPAWN:_RandomizeTemplate( SpawnIndex ) return self end +--- Private method that sets the DCS#Vec2 where the Group will be spawned. +-- @param #SPAWN self +-- @param #number SpawnIndex +-- @return #SPAWN self +function SPAWN:_SetInitialPosition( SpawnIndex ) + self:T( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeZones } ) + + if self.SpawnFromNewPosition then + + self:T( "Preparing Spawn at Vec2 ", self.SpawnInitPosition ) + + local SpawnVec2 = self.SpawnInitPosition + + self:T( { SpawnVec2 = SpawnVec2 } ) + + local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate + + SpawnTemplate.route = SpawnTemplate.route or {} + SpawnTemplate.route.points = SpawnTemplate.route.points or {} + SpawnTemplate.route.points[1] = SpawnTemplate.route.points[1] or {} + SpawnTemplate.route.points[1].x = SpawnTemplate.route.points[1].x or 0 + SpawnTemplate.route.points[1].y = SpawnTemplate.route.points[1].y or 0 + + self:T( { Route = SpawnTemplate.route } ) + + for UnitID = 1, #SpawnTemplate.units do + local UnitTemplate = SpawnTemplate.units[UnitID] + self:T( 'Before Translation SpawnTemplate.units[' .. UnitID .. '].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. UnitTemplate.y ) + local SX = UnitTemplate.x + local SY = UnitTemplate.y + local BX = SpawnTemplate.route.points[1].x + local BY = SpawnTemplate.route.points[1].y + local TX = SpawnVec2.x + (SX - BX) + local TY = SpawnVec2.y + (SY - BY) + UnitTemplate.x = TX + UnitTemplate.y = TY + -- TODO: Manage altitude based on landheight... + -- SpawnTemplate.units[UnitID].alt = SpawnVec2: + self:T( 'After Translation SpawnTemplate.units[' .. UnitID .. '].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. UnitTemplate.y ) + end + + SpawnTemplate.route.points[1].x = SpawnVec2.x + SpawnTemplate.route.points[1].y = SpawnVec2.y + SpawnTemplate.x = SpawnVec2.x + SpawnTemplate.y = SpawnVec2.y + + end + + return self +end + --- Private method that randomizes the @{Core.Zone}s where the Group will be spawned. -- @param #SPAWN self -- @param #number SpawnIndex @@ -3415,7 +3510,7 @@ end -- TODO Need to delete this... _DATABASE does this now ... ---- @param #SPAWN self +-- @param #SPAWN self -- @param Core.Event#EVENTDATA EventData function SPAWN:_OnBirth( EventData ) self:F( self.SpawnTemplatePrefix ) @@ -3435,7 +3530,7 @@ function SPAWN:_OnBirth( EventData ) end ---- @param #SPAWN self +-- @param #SPAWN self -- @param Core.Event#EVENTDATA EventData function SPAWN:_OnDeadOrCrash( EventData ) self:F( self.SpawnTemplatePrefix ) From 53fb77b50d902972515bd88ceda0617e6f2d876a Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 13 Jul 2023 16:15:06 +0200 Subject: [PATCH 18/25] #SPAWN --- Moose Development/Moose/Core/Spawn.lua | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index df22111c3..b3f824eb2 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -162,7 +162,17 @@ -- ### Array formation -- -- * @{#SPAWN.InitArray}(): Make groups visible before they are actually activated, and order these groups like a battalion in an array. --- +-- +-- ### Group initial position - if wanted different from template position, for use with e.g. @{#SPAWN.SpawnScheduled}(). +-- +-- * @{#SPAWN.InitPositionCoordinate}(): Set initial position of group via a COORDINATE. +-- * @{#SPAWN.InitPositionVec2}(): Set initial position of group via a VEC2. +-- +-- ### Set the positions of a group's units to absolute positions, or relative positions to unit No. 1 +-- +-- * @{#SPAWN.InitSetUnitRelativePositions}(): Spawn the UNITs of this group with individual relative positions to unit #1 and individual headings. +-- * @{#SPAWN.InitSetUnitAbsolutePositions}(): Spawn the UNITs of this group with individual absolute positions and individual headings. +-- -- ### Position randomization -- -- * @{#SPAWN.InitRandomizePosition}(): Randomizes the position of @{Wrapper.Group}s that are spawned within a **radius band**, given an Outer and Inner radius, from the point that the spawn happens. From 5954b8692f8c6047138edd11e96831a560589c1a Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Sat, 15 Jul 2023 09:36:24 +0200 Subject: [PATCH 19/25] Update Spot.lua Fix --- Moose Development/Moose/Core/Spot.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Core/Spot.lua b/Moose Development/Moose/Core/Spot.lua index 9a12fdf35..af00fee8f 100644 --- a/Moose Development/Moose/Core/Spot.lua +++ b/Moose Development/Moose/Core/Spot.lua @@ -357,7 +357,7 @@ do -- @param To -- @return #SPOT function SPOT:onafterLaseOff( From, Event, To ) - self:t({From, Event, To}) + self:T({From, Event, To}) self:T2( {"Stopped lasing for ", self.Target and self.Target:GetName() or "coord", SpotIR = self.SportIR, SpotLaser = self.SpotLaser } ) From 1ad538ea9f2c6a292be7ddf47058ecebd7c2a02b Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Sat, 15 Jul 2023 09:46:55 +0200 Subject: [PATCH 20/25] Commits from master (#1972) * #SPAWN * Update Spot.lua Fix --- Moose Development/Moose/Core/Spot.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Core/Spot.lua b/Moose Development/Moose/Core/Spot.lua index 9a12fdf35..af00fee8f 100644 --- a/Moose Development/Moose/Core/Spot.lua +++ b/Moose Development/Moose/Core/Spot.lua @@ -357,7 +357,7 @@ do -- @param To -- @return #SPOT function SPOT:onafterLaseOff( From, Event, To ) - self:t({From, Event, To}) + self:T({From, Event, To}) self:T2( {"Stopped lasing for ", self.Target and self.Target:GetName() or "coord", SpotIR = self.SportIR, SpotLaser = self.SpotLaser } ) From 6481f66e27a022d51aa7ad7872763c0726991c4e Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 16 Jul 2023 11:27:34 +0200 Subject: [PATCH 21/25] #MANTIS * Added IDs for Current Hill Assets, keyword "CHM" for group names --- Moose Development/Moose/Functional/Mantis.lua | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Functional/Mantis.lua b/Moose Development/Moose/Functional/Mantis.lua index aeef55fa0..3cfbaa4c9 100644 --- a/Moose Development/Moose/Functional/Mantis.lua +++ b/Moose Development/Moose/Functional/Mantis.lua @@ -104,9 +104,13 @@ -- * Silkworm (though strictly speaking this is a surface to ship missile) -- * SA-2, SA-3, SA-5, SA-6, SA-7, SA-8, SA-9, SA-10, SA-11, SA-13, SA-15, SA-19 -- * From HDS (see note on HDS below): SA-2, SA-3, SA-10B, SA-10C, SA-12, SA-17, SA-20A, SA-20B, SA-23, HQ-2 +-- -- * From SMA: RBS98M, RBS70, RBS90, RBS90M, RBS103A, RBS103B, RBS103AM, RBS103BM, Lvkv9040M -- **NOTE** If you are using the Swedish Military Assets (SMA), please note that the **group name** for RBS-SAM types also needs to contain the keyword "SMA" -- +-- * From CH: 2S38, PantsirS1, PantsirS2, PGL-625, HQ-17A, M903PAC2, M903PAC3, TorM2, TorM2K, TorM2M, NASAMS3-AMRAAMER, NASAMS3-AIM9X2, C-RAM, PGZ-09, S350-9M100, S350-9M96D +-- **NOTE** If you are using the Military Assets by Currenthill (CH), please note that the **group name** for CH-SAM types also needs to contain the keyword "CHM" +-- -- Following the example started above, an SA-6 site group name should start with "Red SAM SA-6" then, or a blue Patriot installation with e.g. "Blue SAM Patriot". -- **NOTE** If you are using the High-Digit-Sam Mod, please note that the **group name** for the following SAM types also needs to contain the keyword "HDS": -- @@ -416,6 +420,35 @@ MANTIS.SamDataSMA = { ["Lvkv9040M SMA"] = { Range=4, Blindspot=0, Height=2.5, Type="Short", Radar="LvKv9040" }, } +--- SAM data CH +-- @type MANTIS.SamDataCH +-- @field #number Range Max firing range in km +-- @field #number Blindspot no-firing range (green circle) +-- @field #number Height Max firing height in km +-- @field #string Type #MANTIS.SamType of SAM, i.e. SHORT, MEDIUM or LONG (range) +-- @field #string Radar Radar typename on unit level (used as key) +MANTIS.SamDataCH = { + -- units from CH (Military Assets by Currenthill) + -- https://www.currenthill.com/ + -- group name MUST contain CHM to ID launcher type correctly! + ["2S38 CH"] = { Range=8, Blindspot=0.5, Height=6, Type="Short", Radar="2S38" }, + ["PantsirS1 CH"] = { Range=20, Blindspot=1.2, Height=15, Type="Short", Radar="PantsirS1" }, + ["PantsirS2 CH"] = { Range=30, Blindspot=1.2, Height=18, Type="Medium", Radar="PantsirS2" }, + ["PGL-625 CH"] = { Range=10, Blindspot=0.5, Height=5, Type="Short", Radar="PGL_625" }, + ["HQ-17A CH"] = { Range=20, Blindspot=1.5, Height=10, Type="Short", Radar="HQ17A" }, + ["M903PAC2 CH"] = { Range=160, Blindspot=3, Height=24.5, Type="Long", Radar="MIM104_M903_PAC2" }, + ["M903PAC3 CH"] = { Range=120, Blindspot=1, Height=40, Type="Long", Radar="MIM104_M903_PAC3" }, + ["TorM2 CH"] = { Range=12, Blindspot=1, Height=10, Type="Short", Radar="TorM2" }, + ["TorM2K CH"] = { Range=12, Blindspot=1, Height=10, Type="Short", Radar="TorM2K" }, + ["TorM2M CH"] = { Range=16, Blindspot=1, Height=10, Type="Short", Radar="TorM2M" }, + ["NASAMS3-AMRAAMER CH"] = { Range=50, Blindspot=2, Height=35.7, Type="Medium", Radar="CH_NASAMS3_LN_AMRAAM_ER" }, + ["NASAMS3-AIM9X2 CH"] = { Range=20, Blindspot=0.2, Height=18, Type="Short", Radar="CH_NASAMS3_LN_AIM9X2" }, + ["C-RAM CH"] = { Range=2, Blindspot=0, Height=2, Type="Short", Radar="CH_Centurion_C_RAM" }, + ["PGZ-09 CH"] = { Range=4, Blindspot=0, Height=3, Type="Short", Radar="CH_PGZ09" }, + ["S350-9M100 CH"] = { Range=15, Blindspot=1.5, Height=8, Type="Short", Radar="CH_S350_50P6_9M100" }, + ["S350-9M96D CH"] = { Range=150, Blindspot=2.5, Height=30, Type="Long", Radar="CH_S350_50P6_9M96D" }, +} + ----------------------------------------------------------------------- -- MANTIS System ----------------------------------------------------------------------- @@ -579,7 +612,7 @@ do -- TODO Version -- @field #string version - self.version="0.8.10" + self.version="0.8.11" self:I(string.format("***** Starting MANTIS Version %s *****", self.version)) --- FSM Functions --- @@ -1300,11 +1333,12 @@ do -- @param #string grpname Name of the group -- @param #boolean mod HDS mod flag -- @param #boolean sma SMA mod flag + -- @param #boolean chm CH mod flag -- @return #number range Max firing range -- @return #number height Max firing height -- @return #string type Long, medium or short range -- @return #number blind "blind" spot - function MANTIS:_GetSAMDataFromUnits(grpname,mod,sma) + function MANTIS:_GetSAMDataFromUnits(grpname,mod,sma,chm) self:T(self.lid.."_GetSAMRangeFromUnits") local found = false local range = self.checkradius @@ -1319,6 +1353,8 @@ do SAMData = self.SamDataHDS elseif sma then SAMData = self.SamDataSMA + elseif chm then + SAMData = self.SamDataCH end --self:T("Looking to auto-match for "..grpname) for _,_unit in pairs(units) do @@ -1365,10 +1401,13 @@ do local found = false local HDSmod = false local SMAMod = false + local CHMod = false if string.find(grpname,"HDS",1,true) then HDSmod = true elseif string.find(grpname,"SMA",1,true) then SMAMod = true + elseif string.find(grpname,"CHM",1,true) then + CHMod = true end if self.automode then for idx,entry in pairs(self.SamData) do @@ -1387,8 +1426,8 @@ do end end -- secondary filter if not found - if (not found and self.automode) or HDSmod or SMAMod then - range, height, type = self:_GetSAMDataFromUnits(grpname,HDSmod,SMAMod) + if (not found and self.automode) or HDSmod or SMAMod or CHMod then + range, height, type = self:_GetSAMDataFromUnits(grpname,HDSmod,SMAMod,CHMod) elseif not found then self:E(self.lid .. string.format("*****Could not match radar data for %s! Will default to midrange values!",grpname)) end From 2c8adf58cbdb2c0ff93158a3babe4dc8206b9abf Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 17 Jul 2023 16:26:53 +0200 Subject: [PATCH 22/25] #CLIENTMENU * Rewrite with a different data approach --- Moose Development/Moose/Core/ClientMenu.lua | 767 +++++++++----------- 1 file changed, 330 insertions(+), 437 deletions(-) diff --git a/Moose Development/Moose/Core/ClientMenu.lua b/Moose Development/Moose/Core/ClientMenu.lua index 60a74fe11..d58c62f4b 100644 --- a/Moose Development/Moose/Core/ClientMenu.lua +++ b/Moose Development/Moose/Core/ClientMenu.lua @@ -3,12 +3,14 @@ -- **Main Features:** -- -- * For complex, non-static menu structures --- * Separation of menu tree creation from pushing it to clients +-- * Lightweigt implementation as alternative to MENU +-- * Separation of menu tree creation from menu on the clients's side -- * Works with a SET_CLIENT set of clients -- * Allow manipulation of the shadow tree in various ways -- * Push to all or only one client --- * Change entries' menu text, even if they have a sub-structure --- * Option to make an entry usable once +-- * Change entries' menu text +-- * Option to make an entry usable once only across all clients +-- * Auto appends GROUP and CLIENT objects to menu calls -- -- === -- @@ -37,9 +39,10 @@ -- @field #table parentpath -- @field #CLIENTMENU Parent -- @field Wrapper.Client#CLIENT client --- @field #number GID --- @field #number ID +-- @field #number GroupID Group ID +-- @field #number ID Entry ID -- @field Wrapper.Group#GROUP group +-- @field #string UUID Unique ID based on path+name -- @field #string Function -- @field #table Functionargs -- @field #table Children @@ -54,12 +57,12 @@ CLIENTMENU = { ClassName = "CLIENTMENUE", lid = "", - version = "0.0.1", + version = "0.1.0", name = nil, path = nil, group = nil, client = nil, - GID = nil, + GroupID = nil, Children = {}, Once = false, Generic = false, @@ -87,20 +90,26 @@ function CLIENTMENU:NewEntry(Client,Text,Parent,Function,...) if Client then self.group = Client:GetGroup() self.client = Client - self.GID = self.group:GetID() + self.GroupID = self.group:GetID() else self.Generic = true end self.name = Text or "unknown entry" if Parent then - self.parentpath = Parent:GetPath() - Parent:AddChild(self) + if Parent:IsInstanceOf("MENU_BASE") then + self.parentpath = Parent.MenuPath + else + self.parentpath = Parent:GetPath() + Parent:AddChild(self) + end end self.Parent = Parent self.Function = Function - self.Functionargs = arg + self.Functionargs = arg or {} + table.insert(self.Functionargs,self.group) + table.insert(self.Functionargs,self.client) if self.Functionargs and self.debug then - self:I({"Functionargs",self.Functionargs}) + self:T({"Functionargs",self.Functionargs}) end if not self.Generic then if Function ~= nil then @@ -120,9 +129,9 @@ function CLIENTMENU:NewEntry(Client,Text,Parent,Function,...) self:Clear() end end - self.path = missionCommands.addCommandForGroup(self.GID,Text,self.parentpath, self.CallHandler) + self.path = missionCommands.addCommandForGroup(self.GroupID,Text,self.parentpath, self.CallHandler) else - self.path = missionCommands.addSubMenuForGroup(self.GID,Text,self.parentpath) + self.path = missionCommands.addSubMenuForGroup(self.GroupID,Text,self.parentpath) end else if self.parentpath then @@ -131,8 +140,9 @@ function CLIENTMENU:NewEntry(Client,Text,Parent,Function,...) self.path = {} end self.path[#self.path+1] = Text - self:T({self.path}) end + self.UUID = table.concat(self.path,";") + self:T({self.UUID}) self.Once = false -- Log id. self.lid=string.format("CLIENTMENU %s | %s | ", self.ID, self.name) @@ -140,6 +150,21 @@ function CLIENTMENU:NewEntry(Client,Text,Parent,Function,...) return self end +--- Create a UUID +-- @param #CLIENTMENU self +-- @param #CLIENTMENU Parent The parent object if any +-- @param #string Text The menu entry text +-- @return #string UUID +function CLIENTMENU:CreateUUID(Parent,Text) + local path = {} + if Parent and Parent.path then + path = Parent.path + end + path[#path+1] = Text + local UUID = table.concat(path,";") + return UUID +end + --- Set the CLIENTMENUMANAGER for this entry. -- @param #CLIENTMENU self -- @param #CLIENTMENUMANAGER Controller The controlling object. @@ -163,8 +188,9 @@ end -- @return #CLIENTMENU self function CLIENTMENU:RemoveF10() self:T(self.lid.."RemoveF10") - if not self.Generic then - missionCommands.removeItemForGroup(self.GID , self.path ) + if self.GroupID then + --self:I(self.lid.."Removing "..table.concat(self.path,";")) + missionCommands.removeItemForGroup(self.GroupID , self.path ) end return self end @@ -177,6 +203,14 @@ function CLIENTMENU:GetPath() return self.path end +--- Get the UUID. +-- @param #CLIENTMENU self +-- @return #string UUID +function CLIENTMENU:GetUUID() + self:T(self.lid.."GetUUID") + return self.UUID +end + --- Link a child entry. -- @param #CLIENTMENU self -- @param #CLIENTMENU Child The entry to link as a child. @@ -202,7 +236,7 @@ end -- @return #CLIENTMENU self function CLIENTMENU:RemoveSubEntries() self:T(self.lid.."RemoveSubEntries") - --self:T({self.Children}) + self:T({self.Children}) for _id,_entry in pairs(self.Children) do self:T("Removing ".._id) if _entry then @@ -211,10 +245,10 @@ function CLIENTMENU:RemoveSubEntries() if _entry.Parent then _entry.Parent:RemoveChild(self) end - if self.Controller then - self.Controller:_RemoveByID(_entry.ID) - end - _entry = nil + --if self.Controller then + --self.Controller:_RemoveByID(_entry.ID) + --end + --_entry = nil end end return self @@ -235,9 +269,9 @@ function CLIENTMENU:Clear() if self.Parent then self.Parent:RemoveChild(self) end - if self.Controller then - self.Controller:_RemoveByID(self.ID) - end + --if self.Controller then + --self.Controller:_RemoveByID(self.ID) + --end return self end @@ -256,9 +290,9 @@ end -- @field #string version Version string -- @field #string name Name -- @field Core.Set#SET_CLIENT clientset The set of clients this menu manager is for --- @field #table structure --- @field #table replacementstructure +-- @field #table flattree -- @field #table rootentries +-- @field #table menutree -- @field #number entrycount -- @field #boolean debug -- @extends Core.Base#BASE @@ -310,27 +344,28 @@ end -- -- ## Remove a single entry and also it's subtree -- --- menumgr:Clear(mymenu_lv3a) +-- menumgr:DeleteEntry(mymenu_lv3a) -- -- ## Add a single entry -- -- local baimenu = menumgr:NewEntry("BAI",mymenu_lv1b) --- menumgr:AddEntry(baimenu) +-- +-- menumgr:AddEntry(baimenu) +-- +-- ## Add an entry with a function +-- +-- local baimenu = menumgr:NewEntry("Task Action", mymenu_lv1b, TestFunction, Argument1, Argument1) +-- +-- Now, the class will **automatically append the call with GROUP and CLIENT objects**, as this is can only be done when pushing the entry to the clients. So, the actual function implementation needs to look like this: +-- +-- function TestFunction( Argument1, Argument2, Group, Client) +-- +-- **Caveat is**, that you need to ensure your arguments are not **nil** or **false**, as LUA will optimize those away. You would end up having Group and Client in wrong places in the function call. Hence, +-- if you need/ want to send **nil** or **false**, send a place holder instead and ensure your function can handle this, e.g. +-- +-- local baimenu = menumgr:NewEntry("Task Action", mymenu_lv1b, TestFunction, "nil", Argument1) -- --- ## Prepare and push a partial replacement in the tree --- --- menumgr:PrepareNewReplacementStructure() --- local submenu = menumgr:NewReplacementEntry("New Level 2 ba",mymenu_lv2a) --- menumgr:NewReplacementEntry("New Level 2 bb",mymenu_lv2a) --- menumgr:NewReplacementEntry("Deleted",mymenu_lv2a) --- menumgr:NewReplacementEntry("New Level 2 bd",mymenu_lv2a) --- menumgr:NewReplacementEntry("SubLevel 3 baa",submenu) --- menumgr:NewReplacementEntry("SubLevel 3 bab",submenu) --- menumgr:NewReplacementEntry("SubLevel 3 bac",submenu) --- menumgr:NewReplacementEntry("SubLevel 3 bad",submenu) --- menumgr:ReplaceEntries(mymenu_lv2a) --- --- ## Change the text of an entry in the menu tree +-- ## Change the text of a leaf entry in the menu tree -- -- menumgr:ChangeEntryTextForAll(mymenu_lv1b,"Attack") -- @@ -346,31 +381,17 @@ end CLIENTMENUMANAGER = { ClassName = "CLIENTMENUMANAGER", lid = "", - version = "0.0.1", + version = "0.1.0", name = nil, clientset = nil, - --- - -- @field #CLIENTMENUMANAGER.Structure - structure = { - generic = {}, - IDs = {}, - }, - --- - -- #CLIENTMENUMANAGER.ReplacementStructure - replacementstructure = { - generic = {}, - IDs = {}, - }, + menutree = {}, + flattree = {}, + playertree = {}, entrycount = 0, rootentries = {}, debug = true, } ---- --- @type CLIENTMENUMANAGER.Structure --- @field #table generic --- @field #table IDs - --- Create a new ClientManager instance. -- @param #CLIENTMENUMANAGER self -- @param Core.Set#SET_CLIENT ClientSet The set of clients to manage. @@ -384,7 +405,7 @@ function CLIENTMENUMANAGER:New(ClientSet, Alias) -- Log id. self.lid=string.format("CLIENTMENUMANAGER %s | %s | ", self.version, self.name) if self.debug then - self:I(self.lid.."Created") + self:T(self.lid.."Created") end return self end @@ -400,271 +421,177 @@ function CLIENTMENUMANAGER:NewEntry(Text,Parent,Function,...) self:T(self.lid.."NewEntry "..Text or "None") self.entrycount = self.entrycount + 1 local entry = CLIENTMENU:NewEntry(nil,Text,Parent,Function,unpack(arg)) - self.structure.generic[self.entrycount] = entry - self.structure.IDs[entry.ID] = self.entrycount if not Parent then - self.rootentries[self.entrycount] = self.entrycount + self.rootentries[self.entrycount] = entry end + local depth = #entry.path + if not self.menutree[depth] then self.menutree[depth] = {} end + table.insert(self.menutree[depth],entry.UUID) + self.flattree[entry.UUID] = entry return entry end ---- Find **first** matching entry in the generic structure by the menu text. +--- Check matching entry in the generic structure by UUID. -- @param #CLIENTMENUMANAGER self --- @param #string Text Text of the F10 menu entry. +-- @param #string UUID UUID of the menu entry. +-- @return #boolean Exists +function CLIENTMENUMANAGER:EntryUUIDExists(UUID) + local exists = self.flattree[UUID] and true or false + return exists +end + +--- Find matching entry in the generic structure by UUID. +-- @param #CLIENTMENUMANAGER self +-- @param #string UUID UUID of the menu entry. -- @return #CLIENTMENU Entry The #CLIENTMENU object found or nil. --- @return #number GID GID The GID found or nil. -function CLIENTMENUMANAGER:FindEntryByText(Text) - self:T(self.lid.."FindEntryByText "..Text or "None") +function CLIENTMENUMANAGER:FindEntryByUUID(UUID) + self:T(self.lid.."FindEntryByUUID "..UUID or "None") local entry = nil - local gid = nil - for _gid,_entry in UTILS.spairs(self.structure.generic) do + for _gid,_entry in pairs(self.flattree) do local Entry = _entry -- #CLIENTMENU - if Entry and Entry.name == Text then + if Entry and Entry.UUID == UUID then entry = Entry - gid = _gid end end - return entry, gid -end - ---- Find first matching entry in the generic structure by the GID. --- @param #CLIENTMENUMANAGER self --- @param #number GID The GID of the entry to find. --- @return #CLIENTMENU Entry The #CLIENTMENU object found or nil. -function CLIENTMENUMANAGER:GetEntryByGID(GID) - self:T(self.lid.."GetEntryByGID "..GID or "None") - if GID and type(GID) == "number" then - return self.structure.generic[GID] - else - return nil - end -end - ---- Alter the text of an entry in the generic structure and push to all clients. --- @param #CLIENTMENUMANAGER self --- @param #CLIENTMENU Entry The menu entry. --- @param #string Text Text of the F10 menu entry. --- @return #CLIENTMENUMANAGER self -function CLIENTMENUMANAGER:ChangeEntryTextForAll(Entry,Text) - self:T(self.lid.."ChangeEntryTextForAll "..Text or "None") - for _,_client in pairs(self.clientset.Set) do - local client = _client -- Wrapper.Client#CLIENT - if client and client:IsAlive() then - self:ChangeEntryText(Entry,Text, client) - end - end - return self -end - ---- Alter the text of an entry in the generic structure and push to one specific client. --- @param #CLIENTMENUMANAGER self --- @param #CLIENTMENU Entry The menu entry. --- @param #string Text Text of the F10 menu entry. --- @param Wrapper.Client#CLIENT Client The client for whom to alter the entry --- @return #CLIENTMENUMANAGER self -function CLIENTMENUMANAGER:ChangeEntryText(Entry,Text, Client) - self:T(self.lid.."ChangeEntryText "..Text or "None") - - local text = Text or "none" - local oldtext = Entry.name - Entry.name = text - - local newstructure = {} - local changed = 0 - - local function ChangePath(path,oldtext,newtext) - local newpath = {} - for _id,_text in UTILS.spairs(path) do - local txt = _text - if _text == oldtext then - txt = newtext - end - newpath[_id] = txt - end - return newpath - end - - local function AlterPath(children) - for _,_entry in pairs(children) do - local entry = _entry -- #CLIENTMENU - local newpath = ChangePath(entry.path,oldtext,text) - local newparentpath = ChangePath(entry.parentpath,oldtext,text) - entry.path = nil - entry.parentpath = nil - entry.path = newpath - entry.parentpath = newparentpath - self:T({entry.ID}) - --self:T({entry.parentpath}) - newstructure[entry.ID] = UTILS.DeepCopy(entry) - changed = changed + 1 - if entry.Children and #entry.Children > 0 then - AlterPath(entry.Children) - end - end - end - - -- get the entry - local ID = Entry.ID - local GID = self.structure.IDs[ID] - local playername = Client:GetPlayerName() - local children = self.structure[playername][GID].Children - AlterPath(children) - - self:T("Changed entries: "..changed) - - local NewParent = self:NewEntry(Entry.name,Entry.Parent,Entry.Function,unpack(Entry.Functionargs)) - - for _,_entry in pairs(children) do - self:T("Changed parent for ".._entry.ID.." | GID ".._entry.GID) - local entry = _entry -- #CLIENTMENU - entry.Parent = NewParent - end - - self:PrepareNewReplacementStructure() - - for _,_entry in pairs(newstructure) do - self:T("Changed entry: ".._entry.ID.." | GID ".._entry.GID) - local entry = _entry -- #CLIENTMENU - self:NewReplacementEntry(entry.name,entry.Parent,entry.Function,unpack(entry.Functionargs)) - end - - - self:AddEntry(NewParent) - self:ReplaceEntries(NewParent) - - self:Clear(Entry) - - return self -end - ---- Create a new entry in the replacement structure. --- @param #CLIENTMENUMANAGER self --- @param #string Text Text of the F10 menu entry. --- @param #CLIENTMENU Parent The parent menu entry. --- @param #string Function (optional) Function to call when the entry is used. --- @param ... (optional) Arguments for the Function, comma separated --- @return #CLIENTMENU Entry -function CLIENTMENUMANAGER:NewReplacementEntry(Text,Parent,Function,...) - self:T(self.lid.."NewReplacementEntry "..Text or "None") - self.entrycount = self.entrycount + 1 - local entry = CLIENTMENU:NewEntry(nil,Text,Parent,Function,unpack(arg)) - self.replacementstructure.generic[self.entrycount] = entry - self.replacementstructure.IDs[entry.ID] = self.entrycount - local pID = Parent and Parent.ID or "none" - if self.debug then - self:I("Entry ID = "..self.entrycount.." | Parent ID = "..tostring(pID)) - end - if not Parent then - self.rootentries[self.entrycount] = self.entrycount - end return entry end ---- Prepare a new replacement structure. Deletes the previous one. +--- Find matching entries by text in the generic structure by UUID. -- @param #CLIENTMENUMANAGER self --- @return #CLIENTMENUMANAGER self -function CLIENTMENUMANAGER:PrepareNewReplacementStructure() - self:T(self.lid.."PrepareNewReplacementStructure") - self.replacementstructure = nil -- #CLIENTMENUMANAGER.Structure - self.replacementstructure = { - generic = {}, - IDs = {}, - } - return self -end - ---- [Internal] Merge the replacement structure into the generic structure. --- @param #CLIENTMENUMANAGER self --- @return #CLIENTMENUMANAGER self -function CLIENTMENUMANAGER:_MergeReplacementData() - self:T(self.lid.."_MergeReplacementData") - for _id,_entry in pairs(self.replacementstructure.generic) do - self.structure.generic[_id] = _entry - end - for _id,_entry in pairs(self.replacementstructure.IDs) do - self.structure.IDs[_id] = _entry - end - self:_CleanUpPlayerStructure() - return self -end - ---- Replace entries under the Parent entry with the Replacement structure created prior for all clients. --- @param #CLIENTMENUMANAGER self --- @param #CLIENTMENU Parent The parent entry under which to replace with the new structure. --- @param Wrapper.Client#CLIENT Client (optional) If given, make this change only for this client. In this case the generic structure will not be touched. --- @return #CLIENTMENUMANAGER self -function CLIENTMENUMANAGER:ReplaceEntries(Parent,Client) - self:T(self.lid.."ReplaceEntries") - -- clear Parent substructure - local Set = self.clientset.Set - if Client then - Set = {Client} - else - self:RemoveSubEntries(Parent) - end - for _,_client in pairs(Set) do - local client = _client -- Wrapper.Client#CLIENT - if client and client:IsAlive() then - local playername = client:GetPlayerName() - --self.structure[playername] = {} - for _id,_entry in UTILS.spairs(self.replacementstructure.generic) do - local entry = _entry -- #CLIENTMENU - local parent = Parent - self:T("Posted Parent = "..Parent.ID) - if entry.Parent and entry.Parent.name then - parent = self:_GetParentEntry(self.replacementstructure.generic,entry.Parent.name) or Parent - self:T("Found Parent = "..parent.ID) - end - self.structure[playername][_id] = CLIENTMENU:NewEntry(client,entry.name,parent,entry.Function,unpack(entry.Functionargs)) - self.structure[playername][_id].Once = entry.Once - end +-- @param #string Text Text or partial text of the menu entry to find. +-- @param #CLIENTMENU Parent (Optional) Only find entries under this parent entry. +-- @return #table Table of matching UUIDs of #CLIENTMENU objects +-- @return #table Table of matching #CLIENTMENU objects +-- @return #number Number of matches +function CLIENTMENUMANAGER:FindUUIDsByText(Text,Parent) + self:T(self.lid.."FindUUIDsByText "..Text or "None") + local matches = {} + local entries = {} + local n = 0 + for _uuid,_entry in pairs(self.flattree) do + local Entry = _entry -- #CLIENTMENU + if Parent then + if Entry and string.find(Entry.name,Text) and string.find(Entry.UUID,Parent.UUID) then + table.insert(matches,_uuid) + table.insert(entries,Entry ) + n=n+1 + end + else + if Entry and string.find(Entry.name,Text) then + table.insert(matches,_uuid) + table.insert(entries,Entry ) + n=n+1 + end end end - self:_MergeReplacementData() + return matches, entries, n +end + +--- Find matching entries in the generic structure by the menu text. +-- @param #CLIENTMENUMANAGER self +-- @param #string Text Text or partial text of the F10 menu entry. +-- @param #CLIENTMENU Parent (Optional) Only find entries under this parent entry. +-- @return #table Table of matching #CLIENTMENU objects. +-- @return #number Number of matches +function CLIENTMENUMANAGER:FindEntriesByText(Text,Parent) + self:T(self.lid.."FindEntriesByText "..Text or "None") + local matches, objects, number = self:FindUUIDsByText(Text, Parent) + return objects, number +end + +--- Find matching entries under a parent in the generic structure by UUID. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Parent Find entries under this parent entry. +-- @return #table Table of matching UUIDs of #CLIENTMENU objects +-- @return #table Table of matching #CLIENTMENU objects +-- @return #number Number of matches +function CLIENTMENUMANAGER:FindUUIDsByParent(Parent) + self:T(self.lid.."FindUUIDsByParent") + local matches = {} + local entries = {} + local n = 0 + for _uuid,_entry in pairs(self.flattree) do + local Entry = _entry -- #CLIENTMENU + if Parent then + if Entry and string.find(Entry.UUID,Parent.UUID) then + table.insert(matches,_uuid) + table.insert(entries,Entry ) + n=n+1 + end + end + end + return matches, entries, n +end + +--- Find matching entries in the generic structure under a parent. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Parent Find entries under this parent entry. +-- @return #table Table of matching #CLIENTMENU objects. +-- @return #number Number of matches +function CLIENTMENUMANAGER:FindEntriesByParent(Parent) + self:T(self.lid.."FindEntriesByParent") + local matches, objects, number = self:FindUUIDsByParent(Parent) + return objects, number +end + +--- Alter the text of a leaf entry in the generic structure and push to one specific client's F10 menu. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The menu entry. +-- @param #string Text New Text of the F10 menu entry. +-- @param Wrapper.Client#CLIENT Client (optional) The client for whom to alter the entry, if nil done for all clients. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:ChangeEntryText(Entry, Text, Client) + self:T(self.lid.."ChangeEntryText "..Text or "None") + local newentry = CLIENTMENU:NewEntry(nil,Text,Entry.Parent,Entry.Function,unpack(Entry.Functionargs)) + self:DeleteF10Entry(Entry,Client) + self:DeleteGenericEntry(Entry) + if not Entry.Parent then + self.rootentries[self.entrycount] = newentry + end + local depth = #newentry.path + if not self.menutree[depth] then self.menutree[depth] = {} end + table.insert(self.menutree[depth],newentry.UUID) + self.flattree[newentry.UUID] = newentry + self:AddEntry(newentry,Client) return self end ---- [Internal] Find a parent entry in a given structure by name. --- @param #CLIENTMENUMANAGER self --- @param #table Structure Table of entries. --- @param #string Name Name to find. --- @return #CLIENTMENU Entry -function CLIENTMENUMANAGER:_GetParentEntry(Structure,Name) - self:T(self.lid.."_GetParentEntry") - local found = nil - for _,_entry in pairs(Structure) do - local entry = _entry -- #CLIENTMENU - if entry.name == Name then - found = entry - break - end - end - return found -end - ---- Push the complete menu structure to each of the clients in the set. +--- Push the complete menu structure to each of the clients in the set - refresh the menu tree of the clients. -- @param #CLIENTMENUMANAGER self -- @param Wrapper.Client#CLIENT Client (optional) If given, propagate only for this client. -- @return #CLIENTMENU Entry function CLIENTMENUMANAGER:Propagate(Client) self:T(self.lid.."Propagate") + self:T(Client) local Set = self.clientset.Set if Client then - Set = {Set} + Set = {Client} end + self:ResetMenu(Client) for _,_client in pairs(Set) do local client = _client -- Wrapper.Client#CLIENT if client and client:IsAlive() then local playername = client:GetPlayerName() - self.structure[playername] = {} - for _id,_entry in pairs(self.structure.generic) do - local entry = _entry -- #CLIENTMENU - local parent = nil - if entry.Parent and entry.Parent.name then - parent = self:_GetParentEntry(self.structure[playername],entry.Parent.name) + if not self.playertree[playername] then + self.playertree[playername] = {} + end + for level,branch in pairs (self.menutree) do + self:T("Building branch:" .. level) + for _,leaf in pairs(branch) do + self:T("Building leaf:" .. leaf) + local entry = self:FindEntryByUUID(leaf) + if entry then + self:T("Found generic entry:" .. entry.UUID) + local parent = nil + if entry.Parent and entry.Parent.UUID then + parent = self.playertree[playername][entry.Parent.UUID] or self:FindEntryByUUID(entry.Parent.UUID) + end + self.playertree[playername][entry.UUID] = CLIENTMENU:NewEntry(client,entry.name,parent,entry.Function,unpack(entry.Functionargs)) + self.playertree[playername][entry.UUID].Once = entry.Once + else + self:T("NO generic entry for:" .. leaf) + end end - self.structure[playername][_id] = CLIENTMENU:NewEntry(client,entry.name,parent,entry.Function,unpack(entry.Functionargs)) - self.structure[playername][_id].Once = entry.Once end end end @@ -686,85 +613,79 @@ function CLIENTMENUMANAGER:AddEntry(Entry,Client) local client = _client -- Wrapper.Client#CLIENT if client and client:IsAlive() then local playername = client:GetPlayerName() - local entry = Entry -- #CLIENTMENU - local parent = nil - if entry.Parent and entry.Parent.name then - parent = self:_GetParentEntry(self.structure[playername],entry.Parent.name) - end - self.structure[playername][Entry.ID] = CLIENTMENU:NewEntry(client,entry.name,parent,entry.Function,unpack(entry.Functionargs)) - self.structure[playername][Entry.ID].Once = entry.Once + if Entry then + self:T("Adding generic entry:" .. Entry.UUID) + local parent = nil + if not self.playertree[playername] then + self.playertree[playername] = {} + end + if Entry.Parent and Entry.Parent.UUID then + parent = self.playertree[playername][Entry.Parent.UUID] or self:FindEntryByUUID(Entry.Parent.UUID) + end + self.playertree[playername][Entry.UUID] = CLIENTMENU:NewEntry(client,Entry.name,parent,Entry.Function,unpack(Entry.Functionargs)) + self.playertree[playername][Entry.UUID].Once = Entry.Once + else + self:T("NO generic entry given") + end end end return self end ---- Blank out the menu - remove **all root entries** and all entries below from the client's menus, leaving the generic structure untouched. +--- Blank out the menu - remove **all root entries** and all entries below from the client's F10 menus, leaving the generic structure untouched. -- @param #CLIENTMENUMANAGER self --- @param Wrapper.Client#CLIENT Client +-- @param Wrapper.Client#CLIENT Client (optional) If given, remove only for this client. -- @return #CLIENTMENUMANAGER self function CLIENTMENUMANAGER:ResetMenu(Client) self:T(self.lid.."ResetMenu") for _,_entry in pairs(self.rootentries) do - local RootEntry = self.structure.generic[_entry] - if RootEntry then - self:Clear(RootEntry,Client) + --local RootEntry = self.structure.generic[_entry] + if _entry then + self:DeleteF10Entry(_entry,Client) end end return self end ---- Blank out the menu - remove **all root entries** and all entries below from all clients' menus, and **delete** the generic structure. +--- Blank out the menu - remove **all root entries** and all entries below from all clients' F10 menus, and **delete** the generic structure. -- @param #CLIENTMENUMANAGER self -- @return #CLIENTMENUMANAGER self function CLIENTMENUMANAGER:ResetMenuComplete() self:T(self.lid.."ResetMenuComplete") for _,_entry in pairs(self.rootentries) do - local RootEntry = self.structure.generic[_entry] - if RootEntry then - self:Clear(RootEntry) + --local RootEntry = self.structure.generic[_entry] + if _entry then + self:DeleteF10Entry(_entry) end end - self.structure = nil - self.structure = { - generic = {}, - IDs = {}, - } + self.playertree = nil + self.playertree = {} self.rootentries = nil self.rootentries = {} + self.menutree = nil + self.menutree = {} return self end ---- Remove the entry and all entries below the given entry from the client's menus and the generic structure. +--- Remove the entry and all entries below the given entry from the client's F10 menus. -- @param #CLIENTMENUMANAGER self -- @param #CLIENTMENU Entry The entry to remove -- @param Wrapper.Client#CLIENT Client (optional) If given, make this change only for this client. In this case the generic structure will not be touched. -- @return #CLIENTMENUMANAGER self -function CLIENTMENUMANAGER:Clear(Entry,Client) - self:T(self.lid.."Clear") - local rid = self.structure.IDs[Entry.ID] - if rid then - local generic = self.structure.generic[rid] - local Set = self.clientset.Set - if Client then - Set = {Client} - end - for _,_client in pairs(Set) do - local client = _client -- Wrapper.Client#CLIENT - if client and client:IsAlive() then - local playername = client:GetPlayerName() - local entry = self.structure[playername][rid] -- #CLIENTMENU - if entry then - entry:Clear() - self.structure[playername][rid] = nil - end - end - end - if not Client then - for _id,_entry in pairs(self.structure.generic) do - local entry = _entry -- #CLIENTMENU - if entry and entry.Parent and entry.Parent.ID and entry.Parent.ID == rid then - self.structure.IDs[entry.ID] = nil - entry = nil +function CLIENTMENUMANAGER:DeleteF10Entry(Entry,Client) + self:T(self.lid.."DeleteF10Entry") + local Set = self.clientset.Set + if Client then + Set = {Client} + end + for _,_client in pairs(Set) do + if _client and _client:IsAlive() then + local playername = _client:GetPlayerName() + if self.playertree[playername] then + local centry = self.playertree[playername][Entry.UUID] -- #CLIENTMENU + if centry then + --self:I("Match for "..Entry.UUID) + centry:Clear() end end end @@ -772,115 +693,87 @@ function CLIENTMENUMANAGER:Clear(Entry,Client) return self end ---- [Internal] Clean up player shadow structure +--- Remove the entry and all entries below the given entry from the generic tree. -- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The entry to remove -- @return #CLIENTMENUMANAGER self -function CLIENTMENUMANAGER:_CleanUpPlayerStructure() - self:T(self.lid.."_CleanUpPlayerStructure") - for _,_client in pairs(self.clientset.Set) do - local client = _client -- Wrapper.Client#CLIENT - if client and client:IsAlive() then - local playername = client:GetPlayerName() - local newstructure = {} - for _id, _entry in UTILS.spairs(self.structure[playername]) do - if self.structure.generic[_id] then - newstructure[_id] = _entry - end - end - self.structure[playername] = nil - self.structure[playername] = newstructure +function CLIENTMENUMANAGER:DeleteGenericEntry(Entry) + self:T(self.lid.."DeleteGenericEntry") + + if Entry.Children and #Entry.Children > 0 then + self:RemoveGenericSubEntries(Entry) + end + + local depth = #Entry.path + local uuid = Entry.UUID + + local tbl = UTILS.DeepCopy(self.menutree) + + if tbl[depth] then + for i=depth,#tbl do + --self:I("Level = "..i) + for _id,_uuid in pairs(tbl[i]) do + self:T(_uuid) + if string.find(_uuid,uuid) or _uuid == uuid then + --self:I("Match for ".._uuid) + self.menutree[i][_id] = nil + self.flattree[_uuid] = nil + end + end end end + return self end ---- Remove all entries below the given entry from the clients' menus and the generic structure. +--- Remove all entries below the given entry from the generic tree. -- @param #CLIENTMENUMANAGER self --- @param #CLIENTMENU Entry The menu entry +-- @param #CLIENTMENU Entry The entry where to start. This entry stays. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:RemoveGenericSubEntries(Entry) + self:T(self.lid.."RemoveGenericSubEntries") + + local depth = #Entry.path + 1 + local uuid = Entry.UUID + + local tbl = UTILS.DeepCopy(self.menutree) + + if tbl[depth] then + for i=depth,#tbl do + self:T("Level = "..i) + for _id,_uuid in pairs(tbl[i]) do + self:T(_uuid) + if string.find(_uuid,uuid) then + self:T("Match for ".._uuid) + self.menutree[i][_id] = nil + self.flattree[_uuid] = nil + end + end + end + end + return self +end + + +--- Remove all entries below the given entry from the client's F10 menus. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The entry where to start. This entry stays. -- @param Wrapper.Client#CLIENT Client (optional) If given, make this change only for this client. In this case the generic structure will not be touched. -- @return #CLIENTMENUMANAGER self -function CLIENTMENUMANAGER:RemoveSubEntries(Entry,Client) +function CLIENTMENUMANAGER:RemoveF10SubEntries(Entry,Client) self:T(self.lid.."RemoveSubEntries") - local rid = self.structure.IDs[Entry.ID] - if rid then - local Set = self.clientset.Set - if Client then - Set = {Client} - end - for _,_client in pairs(Set) do - local client = _client -- Wrapper.Client#CLIENT - if client and client:IsAlive() then - local playername = client:GetPlayerName() - local entry = self.structure[playername][rid] -- #CLIENTMENU - if entry then - entry:RemoveSubEntries() - end + local Set = self.clientset.Set + if Client then + Set = {Client} + end + for _,_client in pairs(Set) do + if _client and _client:IsAlive() then + local playername = _client:GetPlayerName() + if self.playertree[playername] then + local centry = self.playertree[playername][Entry.UUID] -- #CLIENTMENU + centry:RemoveSubEntries() end end - if not Client then - for _id,_entry in pairs(self.structure.generic) do - local entry = _entry -- #CLIENTMENU - if entry and entry.Parent and entry.Parent.ID and entry.Parent.ID == rid then - self.structure.IDs[entry.ID] = nil - self.structure.generic[_id] = nil - end - end - end - end - self:_CleanUpPlayerStructure() - return self -end - ---- Remove a specific entry by ID from the generic structure --- @param #CLIENTMENUMANAGER self --- @param #number ID --- @return #CLIENTMENUMANAGER self -function CLIENTMENUMANAGER:_RemoveByID(ID) - self:T(self.lid.."_RemoveByID "..ID or "none") - if ID then - local gid = self.structure.IDs[ID] - if gid then - self.structure.generic[gid] = nil - self.structure.IDs[ID] = nil - end - end - return self -end - ---- [Internal] Dump structures to log for debug --- @param #CLIENTMENUMANAGER self --- @param #string Playername --- @return #CLIENTMENUMANAGER self -function CLIENTMENUMANAGER:_CheckStructures(Playername) - self:T(self.lid.."CheckStructures") - self:I("Generic Structure") - self:I("-----------------") - for _id,_entry in UTILS.spairs(self.structure.generic) do - local ID = "none" - if _entry and _entry.ID then - ID = _entry.ID - end - self:I("ID= ".._id.." | EntryID = "..ID) - if _id > 10 and _id < 14 then - self:I(_entry.name) - end - end - self:I("Reverse Structure") - self:I("-----------------") - for _id,_entry in UTILS.spairs(self.structure.IDs) do - self:I("EntryID= ".._id.." | ID = ".._entry) - end - if Playername then - self:I("Player Structure") - self:I("-----------------") - for _id,_entry in UTILS.spairs(self.structure[Playername]) do - local ID = "none" - if _entry and _entry.ID then - ID = _entry.ID - end - local _lid = _id or "none" - self:I("ID= ".._lid.." | EntryID = "..ID) - end end return self end From 8a8e40e810b00f356a321a490eae39c466d661e8 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 17 Jul 2023 16:27:54 +0200 Subject: [PATCH 23/25] #PLAYERTASK * Refactored menubuilds to use new CLIENTMENU/CLIENTMENUCONTROLLER classes --- Moose Development/Moose/Ops/PlayerTask.lua | 707 ++++++++++++--------- 1 file changed, 402 insertions(+), 305 deletions(-) diff --git a/Moose Development/Moose/Ops/PlayerTask.lua b/Moose Development/Moose/Ops/PlayerTask.lua index a7c127b1f..196e422d0 100644 --- a/Moose Development/Moose/Ops/PlayerTask.lua +++ b/Moose Development/Moose/Ops/PlayerTask.lua @@ -522,7 +522,7 @@ function PLAYERTASK:MarkTargetOnF10Map(Text,Coalition,ReadOnly) -- Marker exists, delete one first self.TargetMarker:Remove() end - local text = Text or "Target of "..self.lid + local text = Text or ("Target of "..self.lid) self.TargetMarker = MARKER:New(coordinate,text) if ReadOnly then self.TargetMarker:ReadOnly() @@ -954,6 +954,7 @@ do -- @field Utilities.FiFo#FIFO TasksPerPlayer -- @field Utilities.FiFo#FIFO PrecisionTasks -- @field Core.Set#SET_CLIENT ClientSet +-- @field Core.Set#SET_CLIENT ActiveClientSet -- @field #string ClientFilter -- @field #string Name -- @field #string Type @@ -988,6 +989,7 @@ do -- @field #table PlayerJoinMenu -- @field #table PlayerInfoMenu -- @field #boolean noflaresmokemenu +-- @field #boolean illumenu -- @field #boolean TransmitOnlyWithPlayers -- @field #boolean buddylasing -- @field Ops.PlayerRecce#PLAYERRECCE PlayerRecce @@ -999,6 +1001,13 @@ do -- @field #table PlayerMenuTag -- @field #boolean UseTypeNames -- @field Functional.Scoring#SCORING Scoring +-- @field Core.ClientMenu#CLIENTMENUMANAGER JoinTaskMenuTemplate +-- @field Core.ClientMenu#CLIENTMENU JoinMenu +-- @field Core.ClientMenu#CLIENTMENU JoinTopMenu +-- @field Core.ClientMenu#CLIENTMENU JoinInfoMenu +-- @field Core.ClientMenu#CLIENTMENUMANAGER ActiveTaskMenuTemplate +-- @field Core.ClientMenu#CLIENTMENU ActiveTopMenu +-- @field Core.ClientMenu#CLIENTMENU ActiveInfoMenu -- @extends Core.Fsm#FSM --- @@ -1155,6 +1164,7 @@ do -- MENUMARK = "Mark on map", -- MENUSMOKE = "Smoke", -- MENUFLARE = "Flare", +-- MENUILLU = "Illuminate", -- MENUABORT = "Abort", -- MENUJOIN = "Join Task", -- MENUTASKINFO = Task Info", @@ -1315,6 +1325,7 @@ PLAYERTASKCONTROLLER = { PlayerInfoMenu = {}, PlayerMenuTag = {}, noflaresmokemenu = false, + illumenu = false, TransmitOnlyWithPlayers = true, buddylasing = false, PlayerRecce = nil, @@ -1408,6 +1419,7 @@ PLAYERTASKCONTROLLER.Messages = { MENUMARK = "Mark on map", MENUSMOKE = "Smoke", MENUFLARE = "Flare", + MENUILLU = "Illuminate", MENUABORT = "Abort", MENUJOIN = "Join Task", MENUTASKINFO = "Task Info", @@ -1487,6 +1499,7 @@ PLAYERTASKCONTROLLER.Messages = { MENUMARK = "Kartenmarkierung", MENUSMOKE = "Rauchgranate", MENUFLARE = "Leuchtgranate", + MENUILLU = "Feldbeleuchtung", MENUABORT = "Abbrechen", MENUJOIN = "Auftrag annehmen", MENUTASKINFO = "Auftrag Briefing", @@ -1592,25 +1605,28 @@ function PLAYERTASKCONTROLLER:New(Name, Coalition, Type, ClientFilter) self.CallsignTranslations = nil self.noflaresmokemenu = false + self.illumenu = false self.ShowMagnetic = true self.UseTypeNames = false - local IsClientSet = false + self.IsClientSet = false if ClientFilter and type(ClientFilter) == "table" and ClientFilter.ClassName and ClientFilter.ClassName == "SET_CLIENT" then -- we have a predefined SET_CLIENT self.ClientSet = ClientFilter - IsClientSet = true + self.IsClientSet = true end - if ClientFilter and not IsClientSet then + if ClientFilter and not self.IsClientSet then self.ClientSet = SET_CLIENT:New():FilterCoalitions(string.lower(self.CoalitionName)):FilterActive(true):FilterPrefixes(ClientFilter):FilterStart() - elseif not IsClientSet then + elseif not self.IsClientSet then self.ClientSet = SET_CLIENT:New():FilterCoalitions(string.lower(self.CoalitionName)):FilterActive(true):FilterStart() end + self.ActiveClientSet = SET_CLIENT:New() + self.lid=string.format("PlayerTaskController %s %s | ", self.Name, tostring(self.Type)) self:_InitLocalization() @@ -1849,6 +1865,24 @@ function PLAYERTASKCONTROLLER:SetEnableSmokeFlareTask() return self end +--- [User] Show menu entries to illuminate targets. Needs smoke/flare enabled. +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetEnableIlluminateTask() + self:T(self.lid.."SetEnableSmokeFlareTask") + self.illumenu = true + return self +end + +--- [User] Do not show menu entries to illuminate targets. +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetDisableIlluminateTask() + self:T(self.lid.."SetDisableIlluminateTask") + self.illumenu = false + return self +end + --- [User] Show info text on screen with a coordinate info in any case (OFF by default) -- @param #PLAYERTASKCONTROLLER self -- @param #boolean OnOff Switch on = true or off = false @@ -2141,6 +2175,7 @@ end -- @return #PLAYERTASKCONTROLLER self function PLAYERTASKCONTROLLER:_EventHandler(EventData) self:T(self.lid.."_EventHandler: "..EventData.id) + self:T(self.lid.."_EventHandler: "..EventData.IniPlayerName) if EventData.id == EVENTS.PlayerLeaveUnit or EventData.id == EVENTS.Ejection or EventData.id == EVENTS.Crash or EventData.id == EVENTS.PilotDead then if EventData.IniPlayerName then self:T(self.lid.."Event for player: "..EventData.IniPlayerName) @@ -2164,41 +2199,45 @@ function PLAYERTASKCONTROLLER:_EventHandler(EventData) self:T(self.lid..text) end elseif EventData.id == EVENTS.PlayerEnterAircraft and EventData.IniCoalition == self.Coalition then - if EventData.IniPlayerName and EventData.IniGroup and self.UseSRS then - if self.ClientSet:IsNotInSet(CLIENT:FindByName(EventData.IniUnitName)) then + if EventData.IniPlayerName and EventData.IniGroup then + if self.IsClientSet and self.ClientSet:IsNotInSet(CLIENT:FindByName(EventData.IniUnitName)) then + self:T(self.lid.."Client not in SET: "..EventData.IniPlayerName) return self end self:T(self.lid.."Event for player: "..EventData.IniPlayerName) - local frequency = self.Frequency - local freqtext = "" - if type(frequency) == "table" then - freqtext = self.gettext:GetEntry("FREQUENCIES",self.locale) - freqtext = freqtext..table.concat(frequency,", ") - else - local freqt = self.gettext:GetEntry("FREQUENCY",self.locale) - freqtext = string.format(freqt,frequency) - end - local modulation = self.Modulation - if type(modulation) == "table" then modulation = modulation[1] end - modulation = UTILS.GetModulationName(modulation) - local switchtext = self.gettext:GetEntry("BROADCAST",self.locale) - local playername = EventData.IniPlayerName - if EventData.IniGroup then - -- personalized flight name in player naming - if self.customcallsigns[playername] then - self.customcallsigns[playername] = nil + if self.UseSRS then + local frequency = self.Frequency + local freqtext = "" + if type(frequency) == "table" then + freqtext = self.gettext:GetEntry("FREQUENCIES",self.locale) + freqtext = freqtext..table.concat(frequency,", ") + else + local freqt = self.gettext:GetEntry("FREQUENCY",self.locale) + freqtext = string.format(freqt,frequency) end - playername = EventData.IniGroup:GetCustomCallSign(self.ShortCallsign,self.Keepnumber) + local modulation = self.Modulation + if type(modulation) == "table" then modulation = modulation[1] end + modulation = UTILS.GetModulationName(modulation) + local switchtext = self.gettext:GetEntry("BROADCAST",self.locale) + + local playername = EventData.IniPlayerName + if EventData.IniGroup then + -- personalized flight name in player naming + if self.customcallsigns[playername] then + self.customcallsigns[playername] = nil + end + playername = EventData.IniGroup:GetCustomCallSign(self.ShortCallsign,self.Keepnumber) + end + playername = self:_GetTextForSpeech(playername) + --local text = string.format("%s, %s, switch to %s for task assignment!",EventData.IniPlayerName,self.MenuName or self.Name,freqtext) + local text = string.format(switchtext,playername,self.MenuName or self.Name,freqtext) + self.SRSQueue:NewTransmission(text,nil,self.SRS,timer.getAbsTime()+60,2,{EventData.IniGroup},text,30,self.BCFrequency,self.BCModulation) end - playername = self:_GetTextForSpeech(playername) - --local text = string.format("%s, %s, switch to %s for task assignment!",EventData.IniPlayerName,self.MenuName or self.Name,freqtext) - local text = string.format(switchtext,playername,self.MenuName or self.Name,freqtext) - self.SRSQueue:NewTransmission(text,nil,self.SRS,timer.getAbsTime()+60,2,{EventData.IniGroup},text,30,self.BCFrequency,self.BCModulation) if EventData.IniPlayerName then self.PlayerMenu[EventData.IniPlayerName] = nil - --self:_BuildMenus(CLIENT:FindByName(EventData.IniUnitName)) - self:_BuildMenus(CLIENT:FindByPlayerName(EventData.IniPlayerName)) + local player = CLIENT:FindByName(EventData.IniUnitName) + self:_SwitchMenuForClient(player,"Info") end end end @@ -2308,7 +2347,7 @@ function PLAYERTASKCONTROLLER:_GetTasksPerType() self:T(self.lid.."_GetTasksPerType") local tasktypes = self:_GetAvailableTaskTypes() - --self:T({tasktypes}) + --self:I({tasktypes}) -- Sort tasks per threat level first local datatable = self.TaskQueue:GetDataTable() @@ -2327,11 +2366,23 @@ function PLAYERTASKCONTROLLER:_GetTasksPerType() local threat=_data.threat local task = _data.task -- Ops.PlayerTask#PLAYERTASK local type = task.Type + local name = task.Target:GetName() + --self:I(name) if not task:IsDone() then + --self:I(name) table.insert(tasktypes[type],task) end end + --[[ + for _type,_data in pairs(tasktypes) do + self:I("Task Type: ".._type) + for _id,_task in pairs(_data) do + self:I("Task Name: ".._task.Target:GetName()) + end + end + --]] + return tasktypes end @@ -2443,7 +2494,7 @@ function PLAYERTASKCONTROLLER:_CheckTaskQueue() local client = _client --Wrapper.Client#CLIENT local group = client:GetGroup() for _,task in pairs(nexttasks) do - self:_JoinTask(group,client,task,true) + self:_JoinTask(task,true,group,client) end end end @@ -2462,7 +2513,7 @@ end -- @param #PLAYERTASKCONTROLLER self -- @return #PLAYERTASKCONTROLLER self function PLAYERTASKCONTROLLER:_CheckPrecisionTasks() - self:T(self.lid.."_CheckTaskQueue") + self:T(self.lid.."_CheckPrecisionTasks") if self.PrecisionTasks:Count() > 0 and self.precisionbombing then if not self.LasingDrone or self.LasingDrone:IsDead() then -- we need a new drone @@ -2965,15 +3016,20 @@ end --- [Internal] Join a player to a task -- @param #PLAYERTASKCONTROLLER self --- @param Wrapper.Group#GROUP Group --- @param Wrapper.Client#CLIENT Client -- @param Ops.PlayerTask#PLAYERTASK Task -- @param #boolean Force Assign task even if client already has one +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client -- @return #PLAYERTASKCONTROLLER self -function PLAYERTASKCONTROLLER:_JoinTask(Group, Client, Task, Force) +function PLAYERTASKCONTROLLER:_JoinTask(Task, Force, Group, Client) + self:T({Force, Group, Client}) self:T(self.lid.."_JoinTask") + local force = false + if type(Force) == "boolean" then + force = Force + end local playername, ttsplayername = self:_GetPlayerName(Client) - if self.TasksPerPlayer:HasUniqueID(playername) and not Force then + if self.TasksPerPlayer:HasUniqueID(playername) and not force then -- Player already has a task if not self.NoScreenOutput then local text = self.gettext:GetEntry("HAVEACTIVETASK",self.locale) @@ -3004,7 +3060,7 @@ function PLAYERTASKCONTROLLER:_JoinTask(Group, Client, Task, Force) self.TasksPerPlayer:Push(Task,playername) self:__PlayerJoinedTask(1, Group, Client, Task) -- clear menu - self:_BuildMenus(Client,true) + self:_SwitchMenuForClient(Client,"Active",1) end if Task.Type == AUFTRAG.Type.PRECISIONBOMBING then if not self.PrecisionTasks:HasUniqueID(Task.PlayerTaskNr) then @@ -3065,19 +3121,23 @@ end --- [Internal] Show active task info -- @param #PLAYERTASKCONTROLLER self +-- @param Ops.PlayerTask#PLAYERTASK Task -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Client#CLIENT Client --- @param Ops.PlayerTask#PLAYERTASK Task -- @return #PLAYERTASKCONTROLLER self -function PLAYERTASKCONTROLLER:_ActiveTaskInfo(Group, Client, Task) +function PLAYERTASKCONTROLLER:_ActiveTaskInfo(Task, Group, Client) self:T(self.lid.."_ActiveTaskInfo") local playername, ttsplayername = self:_GetPlayerName(Client) local text = "" local textTTS = "" - if self.TasksPerPlayer:HasUniqueID(playername) or Task then + local task = nil + if type(Task) ~= "string" then + task = Task + end + if self.TasksPerPlayer:HasUniqueID(playername) or task then -- NODO: Show multiple? -- Details - local task = Task or self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK + local task = task or self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK local tname = self.gettext:GetEntry("TASKNAME",self.locale) local ttsname = self.gettext:GetEntry("TASKNAMETTS",self.locale) local taskname = string.format(tname,task.Type,task.PlayerTaskNr) @@ -3389,280 +3449,287 @@ function PLAYERTASKCONTROLLER:_AbortTask(Group, Client) if not self.NoScreenOutput then local m=MESSAGE:New(text,15,"Tasking"):ToClient(Client) end - self:_BuildMenus(Client,true) + self:_SwitchMenuForClient(Client,"Info",1) return self end ---- [Internal] Build Task Info Menu + +-- TODO - New Menu Manager +--- [Internal] _UpdateJoinMenuTemplate -- @param #PLAYERTASKCONTROLLER self --- @param Wrapper.Group#GROUP group --- @param Wrapper.Client#CLIENT client --- @param #string playername --- @param Core.Menu#MENU_BASE topmenu --- @param #table tasktypes --- @param #table taskpertype --- @param #string newtag --- @return #table taskinfomenu -function PLAYERTASKCONTROLLER:_BuildTaskInfoMenu(group,client,playername,topmenu,tasktypes,taskpertype,newtag) - self:T(self.lid.."_BuildTaskInfoMenu") - local taskinfomenu = nil - if self.taskinfomenu then - local menutaskinfo = self.gettext:GetEntry("MENUTASKINFO",self.locale) - local taskinfomenu = MENU_GROUP:New(group,menutaskinfo,topmenu):SetTag(newtag) - local ittypes = {} - local itaskmenu = {} - local tnow = timer.getTime() - - for _tasktype,_data in pairs(tasktypes) do - ittypes[_tasktype] = MENU_GROUP:New(group,_tasktype,taskinfomenu):SetTag(newtag) - local tasks = taskpertype[_tasktype] or {} - local n = 0 - for _,_task in pairs(tasks) do - _task = _task -- Ops.PlayerTask#PLAYERTASK - local pilotcount = _task:CountClients() - local newtext = "]" - -- marker for new tasks - if tnow - _task.timestamp < 60 then - newtext = "*]" +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_UpdateJoinMenuTemplate() + self:T("_UpdateJoinMenuTemplate") + if self.TaskQueue:Count() > 0 then + local taskpertype = self:_GetTasksPerType() + local JoinMenu = self.JoinMenu -- Core.ClientMenu#CLIENTMENU + --self:I(JoinMenu.UUID) + local controller = self.JoinTaskMenuTemplate -- Core.ClientMenu#CLIENTMENUMANAGER + local actcontroller = self.ActiveTaskMenuTemplate -- Core.ClientMenu#CLIENTMENUMANAGER + local actinfomenu = self.ActiveInfoMenu + --local entrynumbers = {} + --local existingentries = {} + local maxn = self.menuitemlimit + -- Generate task type menu items + for _type,_ in pairs(taskpertype) do + local found = controller:FindEntriesByText(_type) + --self:I({found}) + if #found == 0 then + local newentry = controller:NewEntry(_type,JoinMenu) + controller:AddEntry(newentry) + if self.JoinInfoMenu then + local newentry = controller:NewEntry(_type,self.JoinInfoMenu) + controller:AddEntry(newentry) end - local menutaskno = self.gettext:GetEntry("MENUTASKNO",self.locale) - local text = string.format("%s %03d [%d%s",menutaskno,_task.PlayerTaskNr,pilotcount,newtext) - if self.UseGroupNames then - local name = _task.Target:GetName() - if name ~= "Unknown" then - text = string.format("%s (%03d) [%d%s",name,_task.PlayerTaskNr,pilotcount,newtext) - end + if actinfomenu then + local newentry = actcontroller:NewEntry(_type,self.ActiveInfoMenu) + actcontroller:AddEntry(newentry) end - if self.UseTypeNames then - if _task.TypeName then - --local name = self.gettext:GetEntry(_task.TypeName,self.locale) - text = string.format("%s (%03d) [%d%s",_task.TypeName,_task.PlayerTaskNr,pilotcount,newtext) - --self:T(self.lid.."Menu text = "..text) - end - end - local taskentry = MENU_GROUP_COMMAND:New(group,text,ittypes[_tasktype],self._ActiveTaskInfo,self,group,client,_task):SetTag(newtag) - --taskentry:SetTag(playername) - itaskmenu[#itaskmenu+1] = taskentry - -- keep max items limit - n = n + 1 - if n >= self.menuitemlimit then - break - end end end + + local typelist = self:_GetAvailableTaskTypes() + -- Slot in Tasks + for _tasktype,_data in pairs(typelist) do + self:T("**** Building for TaskType: ".._tasktype) + --local tasks = taskpertype[_tasktype] or {} + for _,_task in pairs(taskpertype[_tasktype]) do + _task = _task -- Ops.PlayerTask#PLAYERTASK + self:T("**** Building for Task: ".._task.Target:GetName()) + if _task.InMenu then + self:T("**** Task already in Menu ".._task.Target:GetName()) + else + --local pilotcount = _task:CountClients() + --local newtext = "]" + --local tnow = timer.getTime() + -- marker for new tasks + --if tnow - _task.timestamp < 60 then + --newtext = "*]" + --end + local menutaskno = self.gettext:GetEntry("MENUTASKNO",self.locale) + --local text = string.format("%s %03d [%d%s",menutaskno,_task.PlayerTaskNr,pilotcount,newtext) + local text = string.format("%s %03d",menutaskno,_task.PlayerTaskNr) + if self.UseGroupNames then + local name = _task.Target:GetName() + if name ~= "Unknown" then + --text = string.format("%s (%03d) [%d%s",name,_task.PlayerTaskNr,pilotcount,newtext) + text = string.format("%s (%03d)",name,_task.PlayerTaskNr) + end + end + --local taskentry = MENU_GROUP_COMMAND:New(group,text,ttypes[_tasktype],self._JoinTask,self,group,client,_task):SetTag(newtag) + local parenttable, number = controller:FindEntriesByText(_tasktype,JoinMenu) + if number > 0 then + local Parent = parenttable[1] + local matches, count = controller:FindEntriesByParent(Parent) + self:T("***** Join Menu ".._tasktype.. " # of entries: "..count) + if count < self.menuitemlimit then + local taskentry = controller:NewEntry(text,Parent,self._JoinTask,self,_task,"false") + controller:AddEntry(taskentry) + _task.InMenu = true + if not _task.UUIDS then _task.UUIDS = {} end + table.insert(_task.UUIDS,taskentry.UUID) + end + end + if self.JoinInfoMenu then + local parenttable, number = controller:FindEntriesByText(_tasktype,self.JoinInfoMenu) + if number > 0 then + local Parent = parenttable[1] + local matches, count = controller:FindEntriesByParent(Parent) + self:T("***** Join Info Menu ".._tasktype.. " # of entries: "..count) + if count < self.menuitemlimit then + local taskentry = controller:NewEntry(text,Parent,self._ActiveTaskInfo,self,_task) + controller:AddEntry(taskentry) + _task.InMenu = true + if not _task.UUIDS then _task.UUIDS = {} end + table.insert(_task.UUIDS,taskentry.UUID) + end + end + end + if actinfomenu then + local parenttable, number = actcontroller:FindEntriesByText(_tasktype,self.ActiveInfoMenu) + if number > 0 then + local Parent = parenttable[1] + local matches, count = actcontroller:FindEntriesByParent(Parent) + self:T("***** Active Info Menu ".._tasktype.. " # of entries: "..count) + if count < self.menuitemlimit then + local taskentry = actcontroller:NewEntry(text,Parent,self._ActiveTaskInfo,self,_task) + actcontroller:AddEntry(taskentry) + _task.InMenu = true + if not _task.AUUIDS then _task.AUUIDS = {} end + table.insert(_task.AUUIDS,taskentry.UUID) + end + end + end + end + end + end end - return taskinfomenu + return self end ---- [Internal] Build client menus +--- [Internal] _RemoveMenuEntriesForTask -- @param #PLAYERTASKCONTROLLER self --- @param Wrapper.Client#CLIENT Client (optional) build for this client name only --- @param #boolean enforced --- @param #boolean fromsuccess +-- @param #PLAYERTASK Task +-- @param Wrapper.Client#CLIENT Client -- @return #PLAYERTASKCONTROLLER self -function PLAYERTASKCONTROLLER:_BuildMenus(Client,enforced,fromsuccess) - self:T(self.lid.."_BuildMenus") - - if self.MenuBuildLocked and (timer.getAbsTime() - self.MenuBuildLocked < 2) then - self:ScheduleOnce(2,self._BuildMenus,self,Client,enforced,fromsuccess) - return self - else - self.MenuBuildLocked = timer.getAbsTime() - end - - local clients = self.ClientSet:GetAliveSet() - local joinorabort = false - local timedbuild = false - - if Client then - -- client + enforced -- join task or abort - clients = {Client} - enforced = true - joinorabort = true - end - - local tasktypes = self:_GetAvailableTaskTypes() - local taskpertype = self:_GetTasksPerType() - - for _,_client in pairs(clients) do - if _client and _client:IsAlive() then - local client = _client -- Wrapper.Client#CLIENT - local group = client:GetGroup() - local unknown = self.gettext:GetEntry("UNKNOWN",self.locale) - local playername = client:GetPlayerName() or unknown - - local oldtag = self.PlayerMenuTag[playername] - local newtag = playername..timer.getAbsTime() - self.PlayerMenuTag[playername] = newtag - - if group and client then - --- - -- TOPMENU - --- - local taskings = self.gettext:GetEntry("MENUTASKING",self.locale) - local longname = self.Name..taskings..self.Type - local menuname = self.MenuName or longname - local playerhastask = false - - if self:_CheckPlayerHasTask(playername) and not fromsuccess then playerhastask = true end - local topmenu = nil - --local oldmenu = nil - local rebuilddone = false - - self:T("Playerhastask = "..tostring(playerhastask).." Enforced = "..tostring(enforced).." Join or Abort = "..tostring(joinorabort)) - - -- Cases to rebuild menu - -- 1) new player - -- 2) player joined a task, joinorabort = true - -- 3) player left a task, joinorabort = true - -- 4) player has no task, but number of tasks changed, and last build > 30 secs ago - if self.PlayerMenu[playername] then - -- NOT a new player - -- 2)+3) Join or abort? - if joinorabort then - self.PlayerMenu[playername]:RemoveSubMenus() - self.PlayerMenu[playername] = MENU_GROUP:New(group,menuname,self.MenuParent) - self.PlayerMenu[playername]:SetTag(newtag) - topmenu = self.PlayerMenu[playername] - elseif (not playerhastask) or enforced then - -- 4) last build > 30 secs? - local T0 = timer.getAbsTime() - local TDiff = T0-self.PlayerMenu[playername].PTTimeStamp - self:T("TDiff = "..string.format("%.2d",TDiff)) - if TDiff >= self.holdmenutime then - --self.PlayerMenu[playername]:RemoveSubMenus() - --oldmenu = self.PlayerMenu[playername] - --self.PlayerMenu[playername] = nil - - --self.PlayerMenu[playername]:RemoveSubMenus() - --self.PlayerMenu[playername] = MENU_GROUP:New(group,menuname,self.MenuParent) - --self.PlayerMenu[playername]:SetTag(newtag) - --self.PlayerMenu[playername].PTTimeStamp = timer.getAbsTime() - --timedbuild = true - end - topmenu = self.PlayerMenu[playername] - end - else - -- 1) new player# - topmenu = MENU_GROUP:New(group,menuname,self.MenuParent) - self.PlayerMenu[playername] = topmenu - self.PlayerMenu[playername]:SetTag(newtag) - self.PlayerMenu[playername].PTTimeStamp = timer.getAbsTime() - enforced = true +function PLAYERTASKCONTROLLER:_RemoveMenuEntriesForTask(Task,Client) + self:T("_RemoveMenuEntriesForTask") + --self:I("Task name: "..Task.Target:GetName()) + --self:I("Client: "..Client:GetPlayerName()) + if Task then + if Task.UUIDS and self.JoinTaskMenuTemplate then + --self:I("***** JoinTaskMenuTemplate") + UTILS.PrintTableToLog(Task.UUIDS) + local controller = self.JoinTaskMenuTemplate + for _,_uuid in pairs(Task.UUIDS) do + local Entry = controller:FindEntryByUUID(_uuid) + if Entry then + controller:DeleteF10Entry(Entry,Client) + controller:DeleteGenericEntry(Entry) + UTILS.PrintTableToLog(controller.menutree) end - - --- - -- ACTIVE TASK MENU - --- - if playerhastask and enforced then - self:T("Building Active Task Menus for "..playername) - rebuilddone = true - local menuactive = self.gettext:GetEntry("MENUACTIVE",self.locale) - local menuinfo = self.gettext:GetEntry("MENUINFO",self.locale) - local menumark = self.gettext:GetEntry("MENUMARK",self.locale) - local menusmoke = self.gettext:GetEntry("MENUSMOKE",self.locale) - local menuflare = self.gettext:GetEntry("MENUFLARE",self.locale) - local menuabort = self.gettext:GetEntry("MENUABORT",self.locale) - - local active = MENU_GROUP:New(group,menuactive,topmenu):SetTag(newtag) - local info = MENU_GROUP_COMMAND:New(group,menuinfo,active,self._ActiveTaskInfo,self,group,client):SetTag(newtag) - local mark = MENU_GROUP_COMMAND:New(group,menumark,active,self._MarkTask,self,group,client):SetTag(newtag) - if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A then - if self.noflaresmokemenu ~= true then - -- no smoking/flaring here if A2A or designer has set noflaresmokemenu to true - local smoke = MENU_GROUP_COMMAND:New(group,menusmoke,active,self._SmokeTask,self,group,client):SetTag(newtag) - local flare = MENU_GROUP_COMMAND:New(group,menuflare,active,self._FlareTask,self,group,client):SetTag(newtag) - local IsNight = client:GetCoordinate():IsNight() - if IsNight then - local light = MENU_GROUP_COMMAND:New(group,menuflare,active,self._IlluminateTask,self,group,client):SetTag(newtag) - end - end - end - local abort = MENU_GROUP_COMMAND:New(group,menuabort,active,self._AbortTask,self,group,client):SetTag(newtag) - if self.activehasinfomenu and self.taskinfomenu then - self:T("Building Active-Info Menus for "..playername) - if self.PlayerInfoMenu[playername] then - self.PlayerInfoMenu[playername]:RemoveSubMenus(nil,oldtag) - end - self.PlayerInfoMenu[playername] = self:_BuildTaskInfoMenu(group,client,playername,topmenu,tasktypes,taskpertype,newtag) - end - elseif (self.TaskQueue:Count() > 0 and enforced) or (not playerhastask and (timedbuild or joinorabort)) then - self:T("Building Join Menus for "..playername) - rebuilddone = true - --- - -- JOIN TASK MENU - --- - local menujoin = self.gettext:GetEntry("MENUJOIN",self.locale) - - if self.PlayerJoinMenu[playername] then - self.PlayerJoinMenu[playername]:RemoveSubMenus(nil,oldtag) - end - - local joinmenu = MENU_GROUP:New(group,menujoin,topmenu):SetTag(newtag) - self.PlayerJoinMenu[playername] = joinmenu - - local ttypes = {} - local taskmenu = {} - for _tasktype,_data in pairs(tasktypes) do - ttypes[_tasktype] = MENU_GROUP:New(group,_tasktype,joinmenu):SetTag(newtag) - local tasks = taskpertype[_tasktype] or {} - local n = 0 - for _,_task in pairs(tasks) do - _task = _task -- Ops.PlayerTask#PLAYERTASK - local pilotcount = _task:CountClients() - local newtext = "]" - local tnow = timer.getTime() - -- marker for new tasks - if tnow - _task.timestamp < 60 then - newtext = "*]" - end - local menutaskno = self.gettext:GetEntry("MENUTASKNO",self.locale) - local text = string.format("%s %03d [%d%s",menutaskno,_task.PlayerTaskNr,pilotcount,newtext) - if self.UseGroupNames then - local name = _task.Target:GetName() - if name ~= "Unknown" then - text = string.format("%s (%03d) [%d%s",name,_task.PlayerTaskNr,pilotcount,newtext) - end - end - local taskentry = MENU_GROUP_COMMAND:New(group,text,ttypes[_tasktype],self._JoinTask,self,group,client,_task):SetTag(newtag) - --taskentry:SetTag(playername) - taskmenu[#taskmenu+1] = taskentry - n = n + 1 - if n >= self.menuitemlimit then - break - end - end - end - if self.taskinfomenu then - self:T("Building Join-Info Menus for "..playername) - if self.PlayerInfoMenu[playername] then - self.PlayerInfoMenu[playername]:RemoveSubMenus(nil,oldtag) - end - self.PlayerInfoMenu[playername] = self:_BuildTaskInfoMenu(group,client,playername,topmenu,tasktypes,taskpertype,newtag) - end - end - if self.AllowFlash and topmenu ~= nil then - local flashtext = self.gettext:GetEntry("FLASHMENU",self.locale) - local flashmenu = MENU_GROUP_COMMAND:New(group,flashtext,topmenu,self._SwitchFlashing,self,group,client):SetTag(newtag) - end - if self.TaskQueue:Count() == 0 then - self:T("No open tasks info") - local menunotasks = self.gettext:GetEntry("MENUNOTASKS",self.locale) - local joinmenu = MENU_GROUP:New(group,menunotasks,self.PlayerMenu[playername]):SetTag(newtag) - rebuilddone = true - end - --- - -- REFRESH MENU - --- - if rebuilddone then - --self.PlayerMenu[playername]:RemoveSubMenus(nil,oldtag) - --self.PlayerMenu[playername]:Set() - self.PlayerMenu[playername]:Refresh() - end end end + + if Task.AUUIDS and self.ActiveTaskMenuTemplate then + --self:I("***** ActiveTaskMenuTemplate") + UTILS.PrintTableToLog(Task.AUUIDS) + for _,_uuid in pairs(Task.AUUIDS) do + local controller = self.ActiveTaskMenuTemplate + local Entry = controller:FindEntryByUUID(_uuid) + if Entry then + controller:DeleteF10Entry(Entry,Client) + controller:DeleteGenericEntry(Entry) + UTILS.PrintTableToLog(controller.menutree) + end + end + end + + Task.UUIDS = nil + Task.AUUIDS = nil + end + return self +end + +--- [Internal] _CreateJoinMenuTemplate +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_CreateJoinMenuTemplate() + self:T("_CreateActiveTaskMenuTemplate") + + local menujoin = self.gettext:GetEntry("MENUJOIN",self.locale) + local menunotasks = self.gettext:GetEntry("MENUNOTASKS",self.locale) + local flashtext = self.gettext:GetEntry("FLASHMENU",self.locale) + + local JoinTaskMenuTemplate = CLIENTMENUMANAGER:New(self.ClientSet,"JoinTask") + + if not self.JoinTopMenu then + local taskings = self.gettext:GetEntry("MENUTASKING",self.locale) + local longname = self.Name..taskings..self.Type + local menuname = self.MenuName or longname + self.JoinTopMenu = JoinTaskMenuTemplate:NewEntry(menuname,self.MenuParent) + end + + if self.AllowFlash then + JoinTaskMenuTemplate:NewEntry(flashtext,self.JoinTopMenu,self._SwitchFlashing,self) + end + + self.JoinMenu = JoinTaskMenuTemplate:NewEntry(menujoin,self.JoinTopMenu) + + if self.taskinfomenu then + local menutaskinfo = self.gettext:GetEntry("MENUTASKINFO",self.locale) + self.JoinInfoMenu = JoinTaskMenuTemplate:NewEntry(menutaskinfo,self.JoinTopMenu) + end + + if self.TaskQueue:Count() == 0 then + JoinTaskMenuTemplate:NewEntry(menunotasks,self.JoinMenu) + end + + self.JoinTaskMenuTemplate = JoinTaskMenuTemplate + + return self +end + +--- [Internal] _CreateActiveTaskMenuTemplate +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_CreateActiveTaskMenuTemplate() + self:T("_CreateActiveTaskMenuTemplate") + + local menuactive = self.gettext:GetEntry("MENUACTIVE",self.locale) + local menuinfo = self.gettext:GetEntry("MENUINFO",self.locale) + local menumark = self.gettext:GetEntry("MENUMARK",self.locale) + local menusmoke = self.gettext:GetEntry("MENUSMOKE",self.locale) + local menuflare = self.gettext:GetEntry("MENUFLARE",self.locale) + local menuillu = self.gettext:GetEntry("MENUILLU",self.locale) + local menuabort = self.gettext:GetEntry("MENUABORT",self.locale) + + local ActiveTaskMenuTemplate = CLIENTMENUMANAGER:New(self.ActiveClientSet,"ActiveTask") + + if not self.ActiveTopMenu then + local taskings = self.gettext:GetEntry("MENUTASKING",self.locale) + local longname = self.Name..taskings..self.Type + local menuname = self.MenuName or longname + self.ActiveTopMenu = ActiveTaskMenuTemplate:NewEntry(menuname,self.MenuParent) + end + + if self.AllowFlash then + local flashtext = self.gettext:GetEntry("FLASHMENU",self.locale) + ActiveTaskMenuTemplate:NewEntry(flashtext,self.ActiveTopMenu,self._SwitchFlashing,self) + end + + local active = ActiveTaskMenuTemplate:NewEntry(menuactive,self.ActiveTopMenu) + ActiveTaskMenuTemplate:NewEntry(menuinfo,active,self._ActiveTaskInfo,self,"NONE") + ActiveTaskMenuTemplate:NewEntry(menumark,active,self._MarkTask,self) + + if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A and self.noflaresmokemenu ~= true then + ActiveTaskMenuTemplate:NewEntry(menusmoke,active,self._SmokeTask,self) + ActiveTaskMenuTemplate:NewEntry(menuflare,active,self._FlareTask,self) + + if self.illumenu then + ActiveTaskMenuTemplate:NewEntry(menuillu,active,self._IlluminateTask,self) + end + + end + + ActiveTaskMenuTemplate:NewEntry(menuabort,active,self._AbortTask,self) + self.ActiveTaskMenuTemplate = ActiveTaskMenuTemplate + + if self.taskinfomenu and self.activehasinfomenu then + local menutaskinfo = self.gettext:GetEntry("MENUTASKINFO",self.locale) + self.ActiveInfoMenu = ActiveTaskMenuTemplate:NewEntry(menutaskinfo,self.ActiveTopMenu) + end + + return self +end + +--- [Internal] _SwitchMenuForClient +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Client#CLIENT Client The client +-- @param #string MenuType +-- @param #number Delay +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_SwitchMenuForClient(Client,MenuType,Delay) + self:T(self.lid.."_SwitchMenuForClient") + if Delay then + self:ScheduleOnce(Delay,self._SwitchMenuForClient,self,Client,MenuType) + return self + end + if MenuType == "Info" then + self.ClientSet:AddClientsByName(Client:GetName()) + self.ActiveClientSet:Remove(Client:GetName(),true) + self.ActiveTaskMenuTemplate:ResetMenu(Client) + self.JoinTaskMenuTemplate:ResetMenu(Client) + self.JoinTaskMenuTemplate:Propagate(Client) + elseif MenuType == "Active" then + self.ActiveClientSet:AddClientsByName(Client:GetName()) + self.ClientSet:Remove(Client:GetName(),true) + self.ActiveTaskMenuTemplate:ResetMenu(Client) + self.JoinTaskMenuTemplate:ResetMenu(Client) + self.ActiveTaskMenuTemplate:Propagate(Client) + else + self:E(self.lid .."Unknown menu type in _SwitchMenuForClient:"..tostring(MenuType)) end - self.MenuBuildLocked = false return self end @@ -3810,8 +3877,8 @@ end -- @param Core.Menu#MENU_MISSION Menu -- @return #PLAYERTASKCONTROLLER self function PLAYERTASKCONTROLLER:SetParentMenu(Menu) - self:T(self.lid.."SetParentName") - self.MenuParent = Menu + self:T(self.lid.."SetParentMenu") + --self.MenuParent = Menu return self end @@ -3965,6 +4032,20 @@ end -- TODO: FSM Functions PLAYERTASKCONTROLLER ------------------------------------------------------------------------------------------------------------------- +--- [Internal] On after start call +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterStart(From, Event, To) + self:T({From, Event, To}) + self:T(self.lid.."onafterStart") + self:_CreateJoinMenuTemplate() + self:_CreateActiveTaskMenuTemplate() + return self +end + --- [Internal] On after Status call -- @param #PLAYERTASKCONTROLLER self -- @param #string From @@ -3994,7 +4075,7 @@ function PLAYERTASKCONTROLLER:onafterStatus(From, Event, To) end end - self:_BuildMenus(nil,enforcedmenu) + self:_UpdateJoinMenuTemplate() if self.verbose then local text = string.format("%s | New Targets: %02d | Active Tasks: %02d | Active Players: %02d | Assigned Tasks: %02d",self.MenuName, targetcount,taskcount,playercount,assignedtasks) @@ -4041,6 +4122,16 @@ function PLAYERTASKCONTROLLER:onafterTaskCancelled(From, Event, To, Task) taskname = string.format(canceltxttts, self.MenuName or self.Name, Task.PlayerTaskNr, tostring(Task.TTSType)) self.SRSQueue:NewTransmission(taskname,nil,self.SRS,nil,2) end + + local clients=Task:GetClientObjects() + for _,client in pairs(clients) do + self:_RemoveMenuEntriesForTask(Task,client) + --self:_SwitchMenuForClient(client,"Info") + end + for _,client in pairs(clients) do + --self:_RemoveMenuEntriesForTask(Task,client) + self:_SwitchMenuForClient(client,"Info",5) + end return self end @@ -4065,9 +4156,15 @@ function PLAYERTASKCONTROLLER:onafterTaskSuccess(From, Event, To, Task) taskname = string.format(succtxttts, self.MenuName or self.Name, Task.PlayerTaskNr, tostring(Task.TTSType)) self.SRSQueue:NewTransmission(taskname,nil,self.SRS,nil,2) end + local clients=Task:GetClientObjects() for _,client in pairs(clients) do - self:_BuildMenus(client,true,true) + self:_RemoveMenuEntriesForTask(Task,client) + --self:_SwitchMenuForClient(client,"Info") + end + for _,client in pairs(clients) do + -- self:_RemoveMenuEntriesForTask(Task,client) + self:_SwitchMenuForClient(client,"Info",5) end return self end From 15bf379cdc405a0da0177a96fcdb7da60f0acaa4 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Wed, 19 Jul 2023 09:56:27 +0200 Subject: [PATCH 24/25] #AICSAR * Added FSM Event "HeloOnDuty" --- Moose Development/Moose/Functional/AICSAR.lua | 408 ++---------------- 1 file changed, 33 insertions(+), 375 deletions(-) diff --git a/Moose Development/Moose/Functional/AICSAR.lua b/Moose Development/Moose/Functional/AICSAR.lua index c9ec13298..922ae567e 100644 --- a/Moose Development/Moose/Functional/AICSAR.lua +++ b/Moose Development/Moose/Functional/AICSAR.lua @@ -1,8 +1,6 @@ ---- **Functional** - AI CSAR system. --- --- === +--- **Functional** - AI CSAR system -- --- ## Features: +-- ## Main Features: -- -- * Send out helicopters to downed pilots -- * Rescues players and AI alike @@ -11,7 +9,6 @@ -- * Dedicated MASH zone -- * Some FSM functions to include in your mission scripts -- * Limit number of available helos --- * SRS voice output via TTS or soundfiles -- -- === -- @@ -21,8 +18,8 @@ -- -- === -- --- ### Author: **Applevangelist** --- Last Update February 2022 +-- ### Author: **applevangelist** +-- Last Update April 2022 -- -- === -- @module Functional.AICSAR @@ -52,11 +49,6 @@ -- @field Core.Set#SET_CLIENT playerset Track if alive heli pilots are available. -- @field #boolean limithelos limit available number of helos going on mission (defaults to true) -- @field #number helonumber number of helos available (default: 3) --- @field Utilities.FiFo#FIFO PilotStore --- @field #number Altitude Default altitude setting for the helicopter FLIGHTGROUP 1500ft. --- @field #number Speed Default speed setting for the helicopter FLIGHTGROUP is 100kn. --- @field #boolean UseEventEject In case Event LandingAfterEjection isn't working, use set this to true. --- @field #number Delay In case of UseEventEject wait this long until we spawn a landed pilot. -- @extends Core.Fsm#FSM @@ -82,7 +74,7 @@ -- -- @param #string Alias Name of this instance. -- -- @param #number Coalition Coalition as in coalition.side.BLUE, can also be passed as "blue", "red" or "neutral" -- -- @param #string Pilottemplate Pilot template name. --- -- @param #string Helotemplate Helicopter template name. Set the template to "cold start". Hueys work best. +-- -- @param #string Helotemplate Helicopter template name. -- -- @param Wrapper.Airbase#AIRBASE FARP FARP object or Airbase from where to start. -- -- @param Core.Zone#ZONE MASHZone Zone where to drop pilots after rescue. -- local my_aicsar=AICSAR:New("Luftrettung",coalition.side.BLUE,"Downed Pilot","Rescue Helo",AIRBASE:FindByName("Test FARP"),ZONE:New("MASH")) @@ -93,11 +85,10 @@ -- my_aicsar.rescuezoneradius -- landing zone around downed pilot. Defaults to 200m -- my_aicsar.autoonoff -- stop operations when human helicopter pilots are around. Defaults to true. -- my_aicsar.verbose -- text messages to own coalition about ongoing operations. Defaults to true. --- my_aicsar.limithelos -- limit available number of helos going on mission (defaults to true) +-- my_aicsarlimithelos -- limit available number of helos going on mission (defaults to true) -- my_aicsar.helonumber -- number of helos available (default: 3) --- my_aicsar.verbose -- boolean, set to `true`for message output on-screen -- --- ## Radio output options +-- ## Radio options -- -- Radio messages, soundfile names and (for SRS) lengths are defined in three enumerators, so you can customize, localize messages and soundfiles to your liking: -- @@ -107,7 +98,7 @@ -- EN = { -- INITIALOK = "Roger, Pilot, we hear you. Stay where you are, a helo is on the way!", -- INITIALNOTOK = "Sorry, Pilot. You're behind maximum operational distance! Good Luck!", --- PILOTDOWN = "Mayday, mayday, mayday! Pilot down at ", -- note that this will be appended with the position in MGRS +-- PILOTDOWN = "Pilot down at ", -- note that this will be appended with the position -- PILOTKIA = "Pilot KIA!", -- HELODOWN = "CSAR Helo Down!", -- PILOTRESCUED = "Pilot rescued!", @@ -143,31 +134,8 @@ -- }, -- } -- --- ## Radio output via SRS and Text-To-Speech (TTS) --- --- Radio output can be done via SRS and Text-To-Speech. No extra sound files required! --- [Initially, Have a look at the guide on setting up SRS TTS for Moose](https://github.com/FlightControl-Master/MOOSE_GUIDES/blob/master/documents/Moose%20TTS%20Setup%20Guide.pdf). --- The text from the `AICSAR.Messages` table above is converted on the fly to an .ogg-file, which is then played back via SRS on the selected frequency and mdulation. --- Hint - the small black window popping up shortly is visible in Single-Player only. --- --- To set up AICSAR for SRS TTS output, add e.g. the following to your script: --- --- -- setup for google TTS, radio 243 AM, SRS server port 5002 with a google standard-quality voice (google cloud account required) --- my_aicsar:SetSRSTTSRadio(true,"C:\\Program Files\\DCS-SimpleRadio-Standalone",243,radio.modulation.AM,5002,MSRS.Voices.Google.Standard.en_US_Standard_D,"en-US","female","C:\\Program Files\\DCS-SimpleRadio-Standalone\\google.json") --- --- -- alternatively for MS Desktop TTS (voices need to be installed locally first!) --- my_aicsar:SetSRSTTSRadio(true,"C:\\Program Files\\DCS-SimpleRadio-Standalone",243,radio.modulation.AM,5002,MSRS.Voices.Microsoft.Hazel,"en-GB","female") --- --- -- define a different voice for the downed pilot(s) --- my_aicsar:SetPilotTTSVoice(MSRS.Voices.Google.Standard.en_AU_Standard_D,"en-AU","male") --- --- -- define another voice for the operator --- my_aicsar:SetOperatorTTSVoice(MSRS.Voices.Google.Standard.en_GB_Standard_A,"en-GB","female") --- --- ## Radio output via preproduced soundfiles --- -- The easiest way to add a soundfile to your mission is to use the "Sound to..." trigger in the mission editor. This will effectively --- save your sound file inside of the .miz mission file. [Example soundfiles are located on github](https://github.com/FlightControl-Master/MOOSE_SOUND/tree/master/AICSAR) +-- save your sound file inside of the .miz mission file. -- -- To customize or localize your texts and sounds, you can take e.g. the following approach to add a German language version: -- @@ -177,7 +145,7 @@ -- -- Switch on radio transmissions via **either** SRS **or** "normal" DCS radio e.g. like so: -- --- my_aicsar:SetSRSRadio(true,"C:\\Program Files\\DCS-SimpleRadio-Standalone",270,radio.modulation.AM,nil,5002) +-- my_aicsar:SetSRSRadio(true,"C:\\Program Files\\DCS-SimpleRadio-Standalone",270,radio.modulation.AM,5002) -- -- or -- @@ -191,7 +159,7 @@ -- @field #AICSAR AICSAR = { ClassName = "AICSAR", - version = "0.1.14", + version = "0.0.8", lid = "", coalition = coalition.side.BLUE, template = "", @@ -203,7 +171,7 @@ AICSAR = { pilotqueue = {}, pilotindex = 0, helos = {}, - verbose = false, + verbose = true, rescuezoneradius = 200, rescued = {}, autoonoff = true, @@ -224,18 +192,6 @@ AICSAR = { helonumber = 3, gettext = nil, locale ="en", -- default text language - SRSTTSRadio = false, - SRSGoogle = false, - SRSQ = nil, - SRSPilot = nil, - SRSPilotVoice = false, - SRSOperator = nil, - SRSOperatorVoice = false, - PilotStore = nil, - Speed = 100, - Altitude = 1500, - UseEventEject = false, - Delay = 100, } -- TODO Messages @@ -245,7 +201,7 @@ AICSAR.Messages = { EN = { INITIALOK = "Roger, Pilot, we hear you. Stay where you are, a helo is on the way!", INITIALNOTOK = "Sorry, Pilot. You're behind maximum operational distance! Good Luck!", - PILOTDOWN = "Mayday, mayday, mayday! Pilot down at ", + PILOTDOWN = "Pilot down at ", PILOTKIA = "Pilot KIA!", HELODOWN = "CSAR Helo Down!", PILOTRESCUED = "Pilot rescued!", @@ -254,7 +210,7 @@ AICSAR.Messages = { DE = { INITIALOK = "Copy, Pilot, wir hören Sie. Bleiben Sie, wo Sie sind!\nEin Hubschrauber sammelt Sie auf!", INITIALNOTOK = "Verstehe, Pilot. Sie sind zu weit weg von uns.\nViel Glück!", - PILOTDOWN = "Mayday, mayday, mayday! Pilot abgestürzt: ", + PILOTDOWN = "Pilot abgestürzt: ", PILOTKIA = "Pilot gefallen!", HELODOWN = "CSAR Hubschrauber verloren!", PILOTRESCUED = "Pilot gerettet!", @@ -347,15 +303,10 @@ function AICSAR:New(Alias,Coalition,Pilottemplate,Helotemplate,FARP,MASHZone) self.farp = FARP self.farpzone = MASHZone self.playerset = SET_CLIENT:New():FilterActive(true):FilterCategories("helicopter"):FilterStart() - self.UseEventEject = false - self.Delay = 300 -- Radio self.SRS = nil self.SRSRadio = false - self.SRSTTSRadio = false - self.SRSGoogle = false - self.SRSQ = nil self.SRSFrequency = 243 self.SRSPath = "\\" self.SRSModulation = radio.modulation.AM @@ -381,9 +332,6 @@ function AICSAR:New(Alias,Coalition,Pilottemplate,Helotemplate,FARP,MASHZone) -- Set some string id for output to DCS.log file. self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") - --Pilot Store - self.PilotStore = FIFO:New() - -- Start State. self:SetStartState("Stopped") @@ -393,14 +341,12 @@ function AICSAR:New(Alias,Coalition,Pilottemplate,Helotemplate,FARP,MASHZone) self:AddTransition("*", "Status", "*") -- CSAR status update. self:AddTransition("*", "PilotDown", "*") -- Pilot down self:AddTransition("*", "PilotPickedUp", "*") -- Pilot in helo - self:AddTransition("*", "PilotUnloaded", "*") -- Pilot Unloaded from helo self:AddTransition("*", "PilotRescued", "*") -- Pilot Rescued self:AddTransition("*", "PilotKIA", "*") -- Pilot dead self:AddTransition("*", "HeloDown", "*") -- Helo dead self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. - self:HandleEvent(EVENTS.LandingAfterEjection,self._EventHandler) - self:HandleEvent(EVENTS.Ejection,self._EjectEventHandler) + self:HandleEvent(EVENTS.LandingAfterEjection) self:__Start(math.random(2,5)) @@ -454,17 +400,7 @@ function AICSAR:New(Alias,Coalition,Pilottemplate,Helotemplate,FARP,MASHZone) -- @param #AICSAR self -- @param #string From From state. -- @param #string Event Event. - -- @param #string To To state. - -- @param #string PilotName - - --- On after "PilotUnloaded" event. - -- @function [parent=#AICSAR] OnAfterPilotUnloaded - -- @param #AICSAR self - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - -- @param Ops.FlightGroup#FLIGHTGROUP Helo - -- @param Ops.OpsGroup#OPSGROUP OpsGroup + -- @param #string To To state. --- On after "PilotKIA" event. -- @function [parent=#AICSAR] OnAfterPilotKIA @@ -515,7 +451,7 @@ function AICSAR:InitLocalization() return self end ---- [User] Switch sound output on and use SRS output for sound files. +--- [User] Switch sound output on and use SRS -- @param #AICSAR self -- @param #boolean OnOff Switch on (true) or off (false). -- @param #string Path Path to your SRS Server Component, e.g. "E:\\\\Program Files\\\\DCS-SimpleRadio-Standalone" @@ -526,12 +462,10 @@ end -- @return #AICSAR self function AICSAR:SetSRSRadio(OnOff,Path,Frequency,Modulation,SoundPath,Port) self:T(self.lid .. "SetSRSRadio") + self:T(self.lid .. "SetSRSRadio to "..tostring(OnOff)) self.SRSRadio = OnOff and true - self.SRSTTSRadio = false self.SRSFrequency = Frequency or 243 self.SRSPath = Path or "c:\\" - self.SRS:SetLabel("ACSR") - self.SRS:SetCoalition(self.coalition) self.SRSModulation = Modulation or radio.modulation.AM local soundpath = os.getenv('TMP') .. "\\DCS\\Mission\\l10n\\DEFAULT" -- defaults to "l10n/DEFAULT/", i.e. add messages by "Sound to..." in the ME self.SRSSoundPath = SoundPath or soundpath @@ -543,88 +477,6 @@ function AICSAR:SetSRSRadio(OnOff,Path,Frequency,Modulation,SoundPath,Port) return self end ---- [User] Switch sound output on and use SRS-TTS output. The voice will be used across all outputs, unless you define an extra voice for downed pilots and/or the operator. --- See `AICSAR:SetPilotTTSVoice()` and `AICSAR:SetOperatorTTSVoice()` --- @param #AICSAR self --- @param #boolean OnOff Switch on (true) or off (false). --- @param #string Path Path to your SRS Server Component, e.g. "E:\\\\Program Files\\\\DCS-SimpleRadio-Standalone" --- @param #number Frequency (Optional) Defaults to 243 (guard) --- @param #number Modulation (Optional) Radio modulation. Defaults to radio.modulation.AM --- @param #number Port (Optional) Port of the SRS, defaults to 5002. --- @param #string Voice (Optional) The voice to be used. --- @param #string Culture (Optional) The culture to be used, defaults to "en-GB" --- @param #string Gender (Optional) The gender to be used, defaults to "male" --- @param #string GoogleCredentials (Optional) Path to google credentials --- @return #AICSAR self -function AICSAR:SetSRSTTSRadio(OnOff,Path,Frequency,Modulation,Port,Voice,Culture,Gender,GoogleCredentials) - self:T(self.lid .. "SetSRSTTSRadio") - self.SRSTTSRadio = OnOff and true - self.SRSRadio = false - self.SRSFrequency = Frequency or 243 - self.SRSPath = Path or "C:\\Program Files\\DCS-SimpleRadio-Standalone" - self.SRSModulation = Modulation or radio.modulation.AM - self.SRSPort = Port or 5002 - if OnOff then - self.SRS = MSRS:New(Path,Frequency,Modulation,1) - self.SRS:SetPort(self.SRSPort) - self.SRS:SetCoalition(self.coalition) - self.SRS:SetLabel("ACSR") - self.SRS:SetVoice(Voice) - self.SRS:SetCulture(Culture) - self.SRS:SetGender(Gender) - if GoogleCredentials then - self.SRS:SetGoogle(GoogleCredentials) - self.SRSGoogle = true - end - self.SRSQ = MSRSQUEUE:New(self.alias) - end - return self -end - ---- [User] Set SRS TTS Voice of downed pilot. `AICSAR:SetSRSTTSRadio()` needs to be set first! --- @param #AICSAR self --- @param #string Voice The voice to be used, e.g. `MSRS.Voices.Google.Standard.en_US_Standard_J` for Google or `MSRS.Voices.Microsoft.David` for Microsoft. --- Specific voices override culture and gender! --- @param #string Culture (Optional) The culture to be used, defaults to "en-US" --- @param #string Gender (Optional) The gender to be used, defaults to "male" --- @return #AICSAR self -function AICSAR:SetPilotTTSVoice(Voice,Culture,Gender) - self:T(self.lid .. "SetPilotTTSVoice") - self.SRSPilotVoice = true - self.SRSPilot = MSRS:New(self.SRSPath,self.SRSFrequency,self.SRSModulation,1) - self.SRSPilot:SetCoalition(self.coalition) - self.SRSPilot:SetVoice(Voice) - self.SRSPilot:SetCulture(Culture or "en-US") - self.SRSPilot:SetGender(Gender or "male") - self.SRSPilot:SetLabel("PILOT") - if self.SRS.google then - self.SRSPilot:SetGoogle(self.SRS.google) - end - return self -end - ---- [User] Set SRS TTS Voice of the rescue operator. `AICSAR:SetSRSTTSRadio()` needs to be set first! --- @param #AICSAR self --- @param #string Voice The voice to be used, e.g. `MSRS.Voices.Google.Standard.en_US_Standard_J` for Google or `MSRS.Voices.Microsoft.David` for Microsoft. --- Specific voices override culture and gender! --- @param #string Culture (Optional) The culture to be used, defaults to "en-GB" --- @param #string Gender (Optional) The gender to be used, defaults to "female" --- @return #AICSAR self -function AICSAR:SetOperatorTTSVoice(Voice,Culture,Gender) - self:T(self.lid .. "SetOperatorTTSVoice") - self.SRSOperatorVoice = true - self.SRSOperator = MSRS:New(self.SRSPath,self.SRSFrequency,self.SRSModulation,1) - self.SRSOperator:SetCoalition(self.coalition) - self.SRSOperator:SetVoice(Voice) - self.SRSOperator:SetCulture(Culture or "en-GB") - self.SRSOperator:SetGender(Gender or "female") - self.SRSPilot:SetLabel("RESCUE") - if self.SRS.google then - self.SRSOperator:SetGoogle(self.SRS.google) - end - return self -end - --- [User] Switch sound output on and use normale (DCS) radio -- @param #AICSAR self -- @param #boolean OnOff Switch on (true) or off (false). @@ -665,103 +517,11 @@ function AICSAR:DCSRadioBroadcast(Soundfile,Duration,Subtitle) return self end ---- [Internal] Catch the ejection and save the pilot name --- @param #AICSAR self --- @param Core.Event#EVENTDATA EventData --- @return #AICSAR self -function AICSAR:_EjectEventHandler(EventData) - local _event = EventData -- Core.Event#EVENTDATA - if _event.IniPlayerName then - self.PilotStore:Push(_event.IniPlayerName) - self:T(self.lid.."Pilot Ejected: ".._event.IniPlayerName) - if self.UseEventEject then - -- get position and spawn in a template pilot - local _LandingPos = COORDINATE:NewFromVec3(_event.initiator:getPosition().p) - local _country = _event.initiator:getCountry() - local _coalition = coalition.getCountryCoalition( _country ) - local data = UTILS.DeepCopy(EventData) - Unit.destroy(_event.initiator) -- shagrat remove static Pilot model - self:ScheduleOnce(self.Delay,self._DelayedSpawnPilot,self,_LandingPos,_coalition) - end - end - return self -end - ---- [Internal] Spawn a pilot --- @param #AICSAR self --- @param Core.Point#COORDINATE _LandingPos Landing Postion --- @param #number _coalition Coalition side --- @return #AICSAR self -function AICSAR:_DelayedSpawnPilot(_LandingPos,_coalition) - - local distancetofarp = _LandingPos:Get2DDistance(self.farp:GetCoordinate()) - -- Mayday Message - local Text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("PILOTDOWN",self.locale) - local text = "" - local setting = {} - setting.MGRS_Accuracy = self.MGRS_Accuracy - local location = _LandingPos:ToStringMGRS(setting) - local msgtxt = Text..location.."!" - location = string.gsub(location,"MGRS ","") - location = string.gsub(location,"%s+","") - location = string.gsub(location,"([%a%d])","%1;") -- "0 5 1 " - location = string.gsub(location,"0","zero") - location = string.gsub(location,"9","niner") - location = "MGRS;"..location - if self.SRSGoogle then - location = string.format("%s",location) - end - text = Text .. location .. "!" - local ttstext = Text .. location .. "! Repeat! "..location - if _coalition == self.coalition then - if self.verbose then - MESSAGE:New(msgtxt,15,"AICSAR"):ToCoalition(self.coalition) - -- MESSAGE:New(msgtxt,15,"AICSAR"):ToLog() - end - if self.SRSRadio then - local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) - sound:SetPlayWithSRS(true) - self.SRS:PlaySoundFile(sound,2) - elseif self.DCSRadio then - self:DCSRadioBroadcast(Soundfile,Soundlength,text) - elseif self.SRSTTSRadio then - if self.SRSPilotVoice then - self.SRSQ:NewTransmission(ttstext,nil,self.SRSPilot,nil,1) - else - self.SRSQ:NewTransmission(ttstext,nil,self.SRS,nil,1) - end - end - end - - -- further processing - if _coalition == self.coalition and distancetofarp <= self.maxdistance then - -- in reach - self:T(self.lid .. "Spawning new Pilot") - self.pilotindex = self.pilotindex + 1 - local newpilot = SPAWN:NewWithAlias(self.template,string.format("%s-AICSAR-%d",self.template, self.pilotindex)) - newpilot:InitDelayOff() - newpilot:OnSpawnGroup( - function (grp) - self.pilotqueue[self.pilotindex] = grp - end - ) - newpilot:SpawnFromCoordinate(_LandingPos) - - self:__PilotDown(2,_LandingPos,true) - elseif _coalition == self.coalition and distancetofarp > self.maxdistance then - -- out of reach, apologies, too far off - self:T(self.lid .. "Pilot out of reach") - self:__PilotDown(2,_LandingPos,false) - end - return self -end - --- [Internal] Catch the landing after ejection and spawn a pilot in situ. -- @param #AICSAR self -- @param Core.Event#EVENTDATA EventData --- @param #boolean FromEject -- @return #AICSAR self -function AICSAR:_EventHandler(EventData, FromEject) +function AICSAR:OnEventLandingAfterEjection(EventData) self:T(self.lid .. "OnEventLandingAfterEjection ID=" .. EventData.id) -- autorescue on off? @@ -771,39 +531,25 @@ function AICSAR:_EventHandler(EventData, FromEject) end end - if self.UseEventEject and (not FromEject) then return self end - local _event = EventData -- Core.Event#EVENTDATA -- get position and spawn in a template pilot local _LandingPos = COORDINATE:NewFromVec3(_event.initiator:getPosition().p) local _country = _event.initiator:getCountry() local _coalition = coalition.getCountryCoalition( _country ) - + -- DONE: add distance check local distancetofarp = _LandingPos:Get2DDistance(self.farp:GetCoordinate()) -- Mayday Message local Text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("PILOTDOWN",self.locale) local text = "" - local setting = {} - setting.MGRS_Accuracy = self.MGRS_Accuracy - local location = _LandingPos:ToStringMGRS(setting) - local msgtxt = Text..location.."!" - location = string.gsub(location,"MGRS ","") - location = string.gsub(location,"%s+","") - location = string.gsub(location,"([%a%d])","%1;") -- "0 5 1 " - location = string.gsub(location,"0","zero") - location = string.gsub(location,"9","niner") - location = "MGRS;"..location - if self.SRSGoogle then - location = string.format("%s",location) - end - text = Text .. location .. "!" - local ttstext = Text .. location .. "! Repeat! "..location if _coalition == self.coalition then if self.verbose then - MESSAGE:New(msgtxt,15,"AICSAR"):ToCoalition(self.coalition) - -- MESSAGE:New(msgtxt,15,"AICSAR"):ToLog() + local setting = {} + setting.MGRS_Accuracy = self.MGRS_Accuracy + local location = _LandingPos:ToStringMGRS(setting) + text = Text .. location .. "!" + MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) end if self.SRSRadio then local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) @@ -811,12 +557,6 @@ function AICSAR:_EventHandler(EventData, FromEject) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) - elseif self.SRSTTSRadio then - if self.SRSPilotVoice then - self.SRSQ:NewTransmission(ttstext,nil,self.SRSPilot,nil,1) - else - self.SRSQ:NewTransmission(ttstext,nil,self.SRS,nil,1) - end end end @@ -876,15 +616,10 @@ function AICSAR:_InitMission(Pilot,Index) -- Cargo transport assignment. local opstransport=OPSTRANSPORT:New(Pilot, pickupzone, self.farpzone) - local helo = self:_GetFlight() -- inject reservation helo.AICSARReserved = true - helo:SetDefaultAltitude(self.Altitude or 1500) - - helo:SetDefaultSpeed(self.Speed or 100) - -- Cargo transport assignment to first Huey group. helo:AddOpsTransport(opstransport) @@ -897,10 +632,6 @@ function AICSAR:_InitMission(Pilot,Index) self:__HeloDown(2,Helo,Index) end - local function AICHeloUnloaded(Helo,OpsGroup) - self:__PilotUnloaded(2,Helo,OpsGroup) - end - function helo:OnAfterLoadingDone(From,Event,To) AICPickedUp(helo,helo:GetCargoGroups(),Index) end @@ -909,10 +640,6 @@ function AICSAR:_InitMission(Pilot,Index) AICHeloDead(helo,Index) end - function helo:OnAfterUnloaded(From,Event,To,OpsGroupCargo) - AICHeloUnloaded(helo,OpsGroupCargo) - end - self.helos[Index] = helo return self @@ -923,7 +650,7 @@ end -- @param Wrapper.Group#GROUP Pilot The pilot to be rescued. -- @return #boolean outcome function AICSAR:_CheckInMashZone(Pilot) - self:T(self.lid .. "_CheckInMashZone") + self:T(self.lid .. "_CheckQueue") if Pilot:IsInZone(self.farpzone) then return true else @@ -931,26 +658,6 @@ function AICSAR:_CheckInMashZone(Pilot) end end ---- [User] Set default helo speed. Note - AI might have other ideas. Defaults to 100kn. --- @param #AICSAR self --- @param #number Knots Speed in knots. --- @return #AICSAR self -function AICSAR:SetDefaultSpeed(Knots) - self:T(self.lid .. "SetDefaultSpeed") - self.Speed = Knots or 100 - return self -end - ---- [User] Set default helo altitudeAGL. Note - AI might have other ideas. Defaults to 1500ft. --- @param #AICSAR self --- @param #number Feet AGL set in feet. --- @return #AICSAR self -function AICSAR:SetDefaultAltitude(Feet) - self:T(self.lid .. "SetDefaultAltitude") - self.Altitude = Feet or 1500 - return self -end - --- [Internal] Check helo queue -- @param #AICSAR self -- @return #AICSAR self @@ -987,14 +694,12 @@ end --- [Internal] Check pilot queue for next mission -- @param #AICSAR self --- @param Ops.OpsGroup#OPSGROUP OpsGroup -- @return #AICSAR self -function AICSAR:_CheckQueue(OpsGroup) +function AICSAR:_CheckQueue() self:T(self.lid .. "_CheckQueue") for _index, _pilot in pairs(self.pilotqueue) do local classname = _pilot.ClassName and _pilot.ClassName or "NONE" local name = _pilot.GroupName and _pilot.GroupName or "NONE" - local playername = "John Doe" local helocount = self:_CountHelos() --self:T("Looking at " .. classname .. " " .. name) -- find one w/o mission @@ -1002,18 +707,11 @@ function AICSAR:_CheckQueue(OpsGroup) local flightgroup = self.helos[_index] -- Ops.FlightGroup#FLIGHTGROUP -- rescued? if self:_CheckInMashZone(_pilot) then - self:T("Pilot" .. _pilot.GroupName .. " rescued!") - if OpsGroup then - OpsGroup:Despawn(10) - else - _pilot:Destroy(true,10) - end + self:T("Pilot" .. _pilot.GroupName .. " rescued!") + _pilot:Destroy(false) self.pilotqueue[_index] = nil self.rescued[_index] = true - if self.PilotStore:Count() > 0 then - playername = self.PilotStore:Pull() - end - self:__PilotRescued(2,playername) + self:__PilotRescued(2) if flightgroup then flightgroup.AICSARReserved = false end @@ -1067,7 +765,7 @@ end -- @return #AICSAR self function AICSAR:onafterStatus(From, Event, To) self:T({From, Event, To}) - --self:_CheckQueue() + self:_CheckQueue() self:_CheckHelos() self:__Status(30) return self @@ -1114,12 +812,6 @@ function AICSAR:onafterPilotDown(From, Event, To, Coordinate, InReach) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) - elseif self.SRSTTSRadio then - if self.SRSOperatorVoice then - self.SRSQ:NewTransmission(text,nil,self.SRSOperator,nil,1) - else - self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) - end end else local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("INITIALNOTOK",self.locale) @@ -1134,15 +826,8 @@ function AICSAR:onafterPilotDown(From, Event, To, Coordinate, InReach) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) - elseif self.SRSTTSRadio then - if self.SRSOperatorVoice then - self.SRSQ:NewTransmission(text,nil,self.SRSOperator,nil,1) - else - self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) - end end end - self:_CheckQueue() return self end @@ -1164,9 +849,7 @@ function AICSAR:onafterPilotKIA(From, Event, To) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) - elseif self.SRSTTSRadio then - self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) - end + end return self end @@ -1190,12 +873,6 @@ function AICSAR:onafterHeloDown(From, Event, To, Helo, Index) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) - elseif self.SRSTTSRadio then - if self.SRSOperatorVoice then - self.SRSQ:NewTransmission(text,nil,self.SRSOperator,nil,1) - else - self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) - end end local findex = 0 local fhname = Helo:GetName() @@ -1235,9 +912,8 @@ end -- @param #string From -- @param #string Event -- @param #string To --- @param #string PilotName -- @return #AICSAR self -function AICSAR:onafterPilotRescued(From, Event, To, PilotName) +function AICSAR:onafterPilotRescued(From, Event, To) self:T({From, Event, To}) local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("PILOTRESCUED",self.locale) if self.verbose then @@ -1249,26 +925,10 @@ function AICSAR:onafterPilotRescued(From, Event, To, PilotName) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) - elseif self.SRSTTSRadio then - self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) end return self end ---- [Internal] onafterPilotUnloaded --- @param #AICSAR self --- @param #string From --- @param #string Event --- @param #string To --- @param Ops.FlightGroup#FLIGHTGROUP Helo --- @param Ops.OpsGroup#OPSGROUP OpsGroup --- @return #AICSAR self -function AICSAR:onafterPilotUnloaded(From, Event, To, Helo, OpsGroup) - self:T({From, Event, To}) - self:_CheckQueue(OpsGroup) - return self -end - --- [Internal] onafterPilotPickedUp -- @param #AICSAR self -- @param #string From @@ -1290,8 +950,6 @@ function AICSAR:onafterPilotPickedUp(From, Event, To, Helo, CargoTable, Index) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) - elseif self.SRSTTSRadio then - self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) end local findex = 0 local fhname = Helo:GetName() From e2929a78c41bfadb6bb012bcb2ba6998449f8bff Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Wed, 19 Jul 2023 16:06:44 +0200 Subject: [PATCH 25/25] #AICSAR - correct code version --- Moose Development/Moose/Functional/AICSAR.lua | 424 ++++++++++++++++-- 1 file changed, 390 insertions(+), 34 deletions(-) diff --git a/Moose Development/Moose/Functional/AICSAR.lua b/Moose Development/Moose/Functional/AICSAR.lua index 922ae567e..b71050778 100644 --- a/Moose Development/Moose/Functional/AICSAR.lua +++ b/Moose Development/Moose/Functional/AICSAR.lua @@ -1,6 +1,8 @@ ---- **Functional** - AI CSAR system +--- **Functional** - AI CSAR system. +-- +-- === -- --- ## Main Features: +-- ## Features: -- -- * Send out helicopters to downed pilots -- * Rescues players and AI alike @@ -9,6 +11,7 @@ -- * Dedicated MASH zone -- * Some FSM functions to include in your mission scripts -- * Limit number of available helos +-- * SRS voice output via TTS or soundfiles -- -- === -- @@ -18,8 +21,8 @@ -- -- === -- --- ### Author: **applevangelist** --- Last Update April 2022 +-- ### Author: **Applevangelist** +-- Last Update July 2023 -- -- === -- @module Functional.AICSAR @@ -49,6 +52,11 @@ -- @field Core.Set#SET_CLIENT playerset Track if alive heli pilots are available. -- @field #boolean limithelos limit available number of helos going on mission (defaults to true) -- @field #number helonumber number of helos available (default: 3) +-- @field Utilities.FiFo#FIFO PilotStore +-- @field #number Altitude Default altitude setting for the helicopter FLIGHTGROUP 1500ft. +-- @field #number Speed Default speed setting for the helicopter FLIGHTGROUP is 100kn. +-- @field #boolean UseEventEject In case Event LandingAfterEjection isn't working, use set this to true. +-- @field #number Delay In case of UseEventEject wait this long until we spawn a landed pilot. -- @extends Core.Fsm#FSM @@ -74,7 +82,7 @@ -- -- @param #string Alias Name of this instance. -- -- @param #number Coalition Coalition as in coalition.side.BLUE, can also be passed as "blue", "red" or "neutral" -- -- @param #string Pilottemplate Pilot template name. --- -- @param #string Helotemplate Helicopter template name. +-- -- @param #string Helotemplate Helicopter template name. Set the template to "cold start". Hueys work best. -- -- @param Wrapper.Airbase#AIRBASE FARP FARP object or Airbase from where to start. -- -- @param Core.Zone#ZONE MASHZone Zone where to drop pilots after rescue. -- local my_aicsar=AICSAR:New("Luftrettung",coalition.side.BLUE,"Downed Pilot","Rescue Helo",AIRBASE:FindByName("Test FARP"),ZONE:New("MASH")) @@ -85,10 +93,11 @@ -- my_aicsar.rescuezoneradius -- landing zone around downed pilot. Defaults to 200m -- my_aicsar.autoonoff -- stop operations when human helicopter pilots are around. Defaults to true. -- my_aicsar.verbose -- text messages to own coalition about ongoing operations. Defaults to true. --- my_aicsarlimithelos -- limit available number of helos going on mission (defaults to true) +-- my_aicsar.limithelos -- limit available number of helos going on mission (defaults to true) -- my_aicsar.helonumber -- number of helos available (default: 3) +-- my_aicsar.verbose -- boolean, set to `true`for message output on-screen -- --- ## Radio options +-- ## Radio output options -- -- Radio messages, soundfile names and (for SRS) lengths are defined in three enumerators, so you can customize, localize messages and soundfiles to your liking: -- @@ -98,7 +107,7 @@ -- EN = { -- INITIALOK = "Roger, Pilot, we hear you. Stay where you are, a helo is on the way!", -- INITIALNOTOK = "Sorry, Pilot. You're behind maximum operational distance! Good Luck!", --- PILOTDOWN = "Pilot down at ", -- note that this will be appended with the position +-- PILOTDOWN = "Mayday, mayday, mayday! Pilot down at ", -- note that this will be appended with the position in MGRS -- PILOTKIA = "Pilot KIA!", -- HELODOWN = "CSAR Helo Down!", -- PILOTRESCUED = "Pilot rescued!", @@ -134,8 +143,31 @@ -- }, -- } -- +-- ## Radio output via SRS and Text-To-Speech (TTS) +-- +-- Radio output can be done via SRS and Text-To-Speech. No extra sound files required! +-- [Initially, Have a look at the guide on setting up SRS TTS for Moose](https://github.com/FlightControl-Master/MOOSE_GUIDES/blob/master/documents/Moose%20TTS%20Setup%20Guide.pdf). +-- The text from the `AICSAR.Messages` table above is converted on the fly to an .ogg-file, which is then played back via SRS on the selected frequency and mdulation. +-- Hint - the small black window popping up shortly is visible in Single-Player only. +-- +-- To set up AICSAR for SRS TTS output, add e.g. the following to your script: +-- +-- -- setup for google TTS, radio 243 AM, SRS server port 5002 with a google standard-quality voice (google cloud account required) +-- my_aicsar:SetSRSTTSRadio(true,"C:\\Program Files\\DCS-SimpleRadio-Standalone",243,radio.modulation.AM,5002,MSRS.Voices.Google.Standard.en_US_Standard_D,"en-US","female","C:\\Program Files\\DCS-SimpleRadio-Standalone\\google.json") +-- +-- -- alternatively for MS Desktop TTS (voices need to be installed locally first!) +-- my_aicsar:SetSRSTTSRadio(true,"C:\\Program Files\\DCS-SimpleRadio-Standalone",243,radio.modulation.AM,5002,MSRS.Voices.Microsoft.Hazel,"en-GB","female") +-- +-- -- define a different voice for the downed pilot(s) +-- my_aicsar:SetPilotTTSVoice(MSRS.Voices.Google.Standard.en_AU_Standard_D,"en-AU","male") +-- +-- -- define another voice for the operator +-- my_aicsar:SetOperatorTTSVoice(MSRS.Voices.Google.Standard.en_GB_Standard_A,"en-GB","female") +-- +-- ## Radio output via preproduced soundfiles +-- -- The easiest way to add a soundfile to your mission is to use the "Sound to..." trigger in the mission editor. This will effectively --- save your sound file inside of the .miz mission file. +-- save your sound file inside of the .miz mission file. [Example soundfiles are located on github](https://github.com/FlightControl-Master/MOOSE_SOUND/tree/master/AICSAR) -- -- To customize or localize your texts and sounds, you can take e.g. the following approach to add a German language version: -- @@ -145,7 +177,7 @@ -- -- Switch on radio transmissions via **either** SRS **or** "normal" DCS radio e.g. like so: -- --- my_aicsar:SetSRSRadio(true,"C:\\Program Files\\DCS-SimpleRadio-Standalone",270,radio.modulation.AM,5002) +-- my_aicsar:SetSRSRadio(true,"C:\\Program Files\\DCS-SimpleRadio-Standalone",270,radio.modulation.AM,nil,5002) -- -- or -- @@ -159,7 +191,7 @@ -- @field #AICSAR AICSAR = { ClassName = "AICSAR", - version = "0.0.8", + version = "0.1.15", lid = "", coalition = coalition.side.BLUE, template = "", @@ -171,7 +203,7 @@ AICSAR = { pilotqueue = {}, pilotindex = 0, helos = {}, - verbose = true, + verbose = false, rescuezoneradius = 200, rescued = {}, autoonoff = true, @@ -192,6 +224,18 @@ AICSAR = { helonumber = 3, gettext = nil, locale ="en", -- default text language + SRSTTSRadio = false, + SRSGoogle = false, + SRSQ = nil, + SRSPilot = nil, + SRSPilotVoice = false, + SRSOperator = nil, + SRSOperatorVoice = false, + PilotStore = nil, + Speed = 100, + Altitude = 1500, + UseEventEject = false, + Delay = 100, } -- TODO Messages @@ -201,7 +245,7 @@ AICSAR.Messages = { EN = { INITIALOK = "Roger, Pilot, we hear you. Stay where you are, a helo is on the way!", INITIALNOTOK = "Sorry, Pilot. You're behind maximum operational distance! Good Luck!", - PILOTDOWN = "Pilot down at ", + PILOTDOWN = "Mayday, mayday, mayday! Pilot down at ", PILOTKIA = "Pilot KIA!", HELODOWN = "CSAR Helo Down!", PILOTRESCUED = "Pilot rescued!", @@ -210,7 +254,7 @@ AICSAR.Messages = { DE = { INITIALOK = "Copy, Pilot, wir hören Sie. Bleiben Sie, wo Sie sind!\nEin Hubschrauber sammelt Sie auf!", INITIALNOTOK = "Verstehe, Pilot. Sie sind zu weit weg von uns.\nViel Glück!", - PILOTDOWN = "Pilot abgestürzt: ", + PILOTDOWN = "Mayday, mayday, mayday! Pilot abgestürzt: ", PILOTKIA = "Pilot gefallen!", HELODOWN = "CSAR Hubschrauber verloren!", PILOTRESCUED = "Pilot gerettet!", @@ -303,10 +347,15 @@ function AICSAR:New(Alias,Coalition,Pilottemplate,Helotemplate,FARP,MASHZone) self.farp = FARP self.farpzone = MASHZone self.playerset = SET_CLIENT:New():FilterActive(true):FilterCategories("helicopter"):FilterStart() + self.UseEventEject = false + self.Delay = 300 -- Radio self.SRS = nil self.SRSRadio = false + self.SRSTTSRadio = false + self.SRSGoogle = false + self.SRSQ = nil self.SRSFrequency = 243 self.SRSPath = "\\" self.SRSModulation = radio.modulation.AM @@ -332,6 +381,9 @@ function AICSAR:New(Alias,Coalition,Pilottemplate,Helotemplate,FARP,MASHZone) -- Set some string id for output to DCS.log file. self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") + --Pilot Store + self.PilotStore = FIFO:New() + -- Start State. self:SetStartState("Stopped") @@ -341,12 +393,15 @@ function AICSAR:New(Alias,Coalition,Pilottemplate,Helotemplate,FARP,MASHZone) self:AddTransition("*", "Status", "*") -- CSAR status update. self:AddTransition("*", "PilotDown", "*") -- Pilot down self:AddTransition("*", "PilotPickedUp", "*") -- Pilot in helo + self:AddTransition("*", "PilotUnloaded", "*") -- Pilot Unloaded from helo self:AddTransition("*", "PilotRescued", "*") -- Pilot Rescued self:AddTransition("*", "PilotKIA", "*") -- Pilot dead self:AddTransition("*", "HeloDown", "*") -- Helo dead + self:AddTransition("*", "HeloOnDuty", "*") -- Helo spawnd self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. - self:HandleEvent(EVENTS.LandingAfterEjection) + self:HandleEvent(EVENTS.LandingAfterEjection,self._EventHandler) + self:HandleEvent(EVENTS.Ejection,self._EjectEventHandler) self:__Start(math.random(2,5)) @@ -400,7 +455,17 @@ function AICSAR:New(Alias,Coalition,Pilottemplate,Helotemplate,FARP,MASHZone) -- @param #AICSAR self -- @param #string From From state. -- @param #string Event Event. - -- @param #string To To state. + -- @param #string To To state. + -- @param #string PilotName + + --- On after "PilotUnloaded" event. + -- @function [parent=#AICSAR] OnAfterPilotUnloaded + -- @param #AICSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.FlightGroup#FLIGHTGROUP Helo + -- @param Ops.OpsGroup#OPSGROUP OpsGroup --- On after "PilotKIA" event. -- @function [parent=#AICSAR] OnAfterPilotKIA @@ -409,6 +474,14 @@ function AICSAR:New(Alias,Coalition,Pilottemplate,Helotemplate,FARP,MASHZone) -- @param #string Event Event. -- @param #string To To state. + --- On after "HeloOnDuty" event. + -- @function [parent=#AICSAR] OnAfterHeloOnDuty + -- @param #AICSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Group#GROUP Helo Helo group object + --- On after "HeloDown" event. -- @function [parent=#AICSAR] OnAfterHeloDown -- @param #AICSAR self @@ -451,7 +524,7 @@ function AICSAR:InitLocalization() return self end ---- [User] Switch sound output on and use SRS +--- [User] Switch sound output on and use SRS output for sound files. -- @param #AICSAR self -- @param #boolean OnOff Switch on (true) or off (false). -- @param #string Path Path to your SRS Server Component, e.g. "E:\\\\Program Files\\\\DCS-SimpleRadio-Standalone" @@ -462,10 +535,12 @@ end -- @return #AICSAR self function AICSAR:SetSRSRadio(OnOff,Path,Frequency,Modulation,SoundPath,Port) self:T(self.lid .. "SetSRSRadio") - self:T(self.lid .. "SetSRSRadio to "..tostring(OnOff)) self.SRSRadio = OnOff and true + self.SRSTTSRadio = false self.SRSFrequency = Frequency or 243 self.SRSPath = Path or "c:\\" + self.SRS:SetLabel("ACSR") + self.SRS:SetCoalition(self.coalition) self.SRSModulation = Modulation or radio.modulation.AM local soundpath = os.getenv('TMP') .. "\\DCS\\Mission\\l10n\\DEFAULT" -- defaults to "l10n/DEFAULT/", i.e. add messages by "Sound to..." in the ME self.SRSSoundPath = SoundPath or soundpath @@ -477,6 +552,88 @@ function AICSAR:SetSRSRadio(OnOff,Path,Frequency,Modulation,SoundPath,Port) return self end +--- [User] Switch sound output on and use SRS-TTS output. The voice will be used across all outputs, unless you define an extra voice for downed pilots and/or the operator. +-- See `AICSAR:SetPilotTTSVoice()` and `AICSAR:SetOperatorTTSVoice()` +-- @param #AICSAR self +-- @param #boolean OnOff Switch on (true) or off (false). +-- @param #string Path Path to your SRS Server Component, e.g. "E:\\\\Program Files\\\\DCS-SimpleRadio-Standalone" +-- @param #number Frequency (Optional) Defaults to 243 (guard) +-- @param #number Modulation (Optional) Radio modulation. Defaults to radio.modulation.AM +-- @param #number Port (Optional) Port of the SRS, defaults to 5002. +-- @param #string Voice (Optional) The voice to be used. +-- @param #string Culture (Optional) The culture to be used, defaults to "en-GB" +-- @param #string Gender (Optional) The gender to be used, defaults to "male" +-- @param #string GoogleCredentials (Optional) Path to google credentials +-- @return #AICSAR self +function AICSAR:SetSRSTTSRadio(OnOff,Path,Frequency,Modulation,Port,Voice,Culture,Gender,GoogleCredentials) + self:T(self.lid .. "SetSRSTTSRadio") + self.SRSTTSRadio = OnOff and true + self.SRSRadio = false + self.SRSFrequency = Frequency or 243 + self.SRSPath = Path or "C:\\Program Files\\DCS-SimpleRadio-Standalone" + self.SRSModulation = Modulation or radio.modulation.AM + self.SRSPort = Port or 5002 + if OnOff then + self.SRS = MSRS:New(Path,Frequency,Modulation,1) + self.SRS:SetPort(self.SRSPort) + self.SRS:SetCoalition(self.coalition) + self.SRS:SetLabel("ACSR") + self.SRS:SetVoice(Voice) + self.SRS:SetCulture(Culture) + self.SRS:SetGender(Gender) + if GoogleCredentials then + self.SRS:SetGoogle(GoogleCredentials) + self.SRSGoogle = true + end + self.SRSQ = MSRSQUEUE:New(self.alias) + end + return self +end + +--- [User] Set SRS TTS Voice of downed pilot. `AICSAR:SetSRSTTSRadio()` needs to be set first! +-- @param #AICSAR self +-- @param #string Voice The voice to be used, e.g. `MSRS.Voices.Google.Standard.en_US_Standard_J` for Google or `MSRS.Voices.Microsoft.David` for Microsoft. +-- Specific voices override culture and gender! +-- @param #string Culture (Optional) The culture to be used, defaults to "en-US" +-- @param #string Gender (Optional) The gender to be used, defaults to "male" +-- @return #AICSAR self +function AICSAR:SetPilotTTSVoice(Voice,Culture,Gender) + self:T(self.lid .. "SetPilotTTSVoice") + self.SRSPilotVoice = true + self.SRSPilot = MSRS:New(self.SRSPath,self.SRSFrequency,self.SRSModulation,1) + self.SRSPilot:SetCoalition(self.coalition) + self.SRSPilot:SetVoice(Voice) + self.SRSPilot:SetCulture(Culture or "en-US") + self.SRSPilot:SetGender(Gender or "male") + self.SRSPilot:SetLabel("PILOT") + if self.SRS.google then + self.SRSPilot:SetGoogle(self.SRS.google) + end + return self +end + +--- [User] Set SRS TTS Voice of the rescue operator. `AICSAR:SetSRSTTSRadio()` needs to be set first! +-- @param #AICSAR self +-- @param #string Voice The voice to be used, e.g. `MSRS.Voices.Google.Standard.en_US_Standard_J` for Google or `MSRS.Voices.Microsoft.David` for Microsoft. +-- Specific voices override culture and gender! +-- @param #string Culture (Optional) The culture to be used, defaults to "en-GB" +-- @param #string Gender (Optional) The gender to be used, defaults to "female" +-- @return #AICSAR self +function AICSAR:SetOperatorTTSVoice(Voice,Culture,Gender) + self:T(self.lid .. "SetOperatorTTSVoice") + self.SRSOperatorVoice = true + self.SRSOperator = MSRS:New(self.SRSPath,self.SRSFrequency,self.SRSModulation,1) + self.SRSOperator:SetCoalition(self.coalition) + self.SRSOperator:SetVoice(Voice) + self.SRSOperator:SetCulture(Culture or "en-GB") + self.SRSOperator:SetGender(Gender or "female") + self.SRSPilot:SetLabel("RESCUE") + if self.SRS.google then + self.SRSOperator:SetGoogle(self.SRS.google) + end + return self +end + --- [User] Switch sound output on and use normale (DCS) radio -- @param #AICSAR self -- @param #boolean OnOff Switch on (true) or off (false). @@ -517,11 +674,103 @@ function AICSAR:DCSRadioBroadcast(Soundfile,Duration,Subtitle) return self end ---- [Internal] Catch the landing after ejection and spawn a pilot in situ. +--- [Internal] Catch the ejection and save the pilot name -- @param #AICSAR self -- @param Core.Event#EVENTDATA EventData -- @return #AICSAR self -function AICSAR:OnEventLandingAfterEjection(EventData) +function AICSAR:_EjectEventHandler(EventData) + local _event = EventData -- Core.Event#EVENTDATA + if _event.IniPlayerName then + self.PilotStore:Push(_event.IniPlayerName) + self:T(self.lid.."Pilot Ejected: ".._event.IniPlayerName) + if self.UseEventEject then + -- get position and spawn in a template pilot + local _LandingPos = COORDINATE:NewFromVec3(_event.initiator:getPosition().p) + local _country = _event.initiator:getCountry() + local _coalition = coalition.getCountryCoalition( _country ) + local data = UTILS.DeepCopy(EventData) + Unit.destroy(_event.initiator) -- shagrat remove static Pilot model + self:ScheduleOnce(self.Delay,self._DelayedSpawnPilot,self,_LandingPos,_coalition) + end + end + return self +end + +--- [Internal] Spawn a pilot +-- @param #AICSAR self +-- @param Core.Point#COORDINATE _LandingPos Landing Postion +-- @param #number _coalition Coalition side +-- @return #AICSAR self +function AICSAR:_DelayedSpawnPilot(_LandingPos,_coalition) + + local distancetofarp = _LandingPos:Get2DDistance(self.farp:GetCoordinate()) + -- Mayday Message + local Text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("PILOTDOWN",self.locale) + local text = "" + local setting = {} + setting.MGRS_Accuracy = self.MGRS_Accuracy + local location = _LandingPos:ToStringMGRS(setting) + local msgtxt = Text..location.."!" + location = string.gsub(location,"MGRS ","") + location = string.gsub(location,"%s+","") + location = string.gsub(location,"([%a%d])","%1;") -- "0 5 1 " + location = string.gsub(location,"0","zero") + location = string.gsub(location,"9","niner") + location = "MGRS;"..location + if self.SRSGoogle then + location = string.format("%s",location) + end + text = Text .. location .. "!" + local ttstext = Text .. location .. "! Repeat! "..location + if _coalition == self.coalition then + if self.verbose then + MESSAGE:New(msgtxt,15,"AICSAR"):ToCoalition(self.coalition) + -- MESSAGE:New(msgtxt,15,"AICSAR"):ToLog() + end + if self.SRSRadio then + local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) + sound:SetPlayWithSRS(true) + self.SRS:PlaySoundFile(sound,2) + elseif self.DCSRadio then + self:DCSRadioBroadcast(Soundfile,Soundlength,text) + elseif self.SRSTTSRadio then + if self.SRSPilotVoice then + self.SRSQ:NewTransmission(ttstext,nil,self.SRSPilot,nil,1) + else + self.SRSQ:NewTransmission(ttstext,nil,self.SRS,nil,1) + end + end + end + + -- further processing + if _coalition == self.coalition and distancetofarp <= self.maxdistance then + -- in reach + self:T(self.lid .. "Spawning new Pilot") + self.pilotindex = self.pilotindex + 1 + local newpilot = SPAWN:NewWithAlias(self.template,string.format("%s-AICSAR-%d",self.template, self.pilotindex)) + newpilot:InitDelayOff() + newpilot:OnSpawnGroup( + function (grp) + self.pilotqueue[self.pilotindex] = grp + end + ) + newpilot:SpawnFromCoordinate(_LandingPos) + + self:__PilotDown(2,_LandingPos,true) + elseif _coalition == self.coalition and distancetofarp > self.maxdistance then + -- out of reach, apologies, too far off + self:T(self.lid .. "Pilot out of reach") + self:__PilotDown(2,_LandingPos,false) + end + return self +end + +--- [Internal] Catch the landing after ejection and spawn a pilot in situ. +-- @param #AICSAR self +-- @param Core.Event#EVENTDATA EventData +-- @param #boolean FromEject +-- @return #AICSAR self +function AICSAR:_EventHandler(EventData, FromEject) self:T(self.lid .. "OnEventLandingAfterEjection ID=" .. EventData.id) -- autorescue on off? @@ -531,25 +780,39 @@ function AICSAR:OnEventLandingAfterEjection(EventData) end end + if self.UseEventEject and (not FromEject) then return self end + local _event = EventData -- Core.Event#EVENTDATA -- get position and spawn in a template pilot local _LandingPos = COORDINATE:NewFromVec3(_event.initiator:getPosition().p) local _country = _event.initiator:getCountry() local _coalition = coalition.getCountryCoalition( _country ) - + -- DONE: add distance check local distancetofarp = _LandingPos:Get2DDistance(self.farp:GetCoordinate()) -- Mayday Message local Text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("PILOTDOWN",self.locale) local text = "" + local setting = {} + setting.MGRS_Accuracy = self.MGRS_Accuracy + local location = _LandingPos:ToStringMGRS(setting) + local msgtxt = Text..location.."!" + location = string.gsub(location,"MGRS ","") + location = string.gsub(location,"%s+","") + location = string.gsub(location,"([%a%d])","%1;") -- "0 5 1 " + location = string.gsub(location,"0","zero") + location = string.gsub(location,"9","niner") + location = "MGRS;"..location + if self.SRSGoogle then + location = string.format("%s",location) + end + text = Text .. location .. "!" + local ttstext = Text .. location .. "! Repeat! "..location if _coalition == self.coalition then if self.verbose then - local setting = {} - setting.MGRS_Accuracy = self.MGRS_Accuracy - local location = _LandingPos:ToStringMGRS(setting) - text = Text .. location .. "!" - MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) + MESSAGE:New(msgtxt,15,"AICSAR"):ToCoalition(self.coalition) + -- MESSAGE:New(msgtxt,15,"AICSAR"):ToLog() end if self.SRSRadio then local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) @@ -557,6 +820,12 @@ function AICSAR:OnEventLandingAfterEjection(EventData) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) + elseif self.SRSTTSRadio then + if self.SRSPilotVoice then + self.SRSQ:NewTransmission(ttstext,nil,self.SRSPilot,nil,1) + else + self.SRSQ:NewTransmission(ttstext,nil,self.SRS,nil,1) + end end end @@ -593,6 +862,11 @@ function AICSAR:_GetFlight() local newhelo = SPAWN:NewWithAlias(self.helotemplate,self.helotemplate..math.random(1,10000)) :InitDelayOff() :InitUnControlled(true) + :OnSpawnGroup( + function(Group) + self:__HeloOnDuty(1,Group) + end + ) :Spawn() local nhelo=FLIGHTGROUP:New(newhelo) @@ -616,10 +890,15 @@ function AICSAR:_InitMission(Pilot,Index) -- Cargo transport assignment. local opstransport=OPSTRANSPORT:New(Pilot, pickupzone, self.farpzone) + local helo = self:_GetFlight() -- inject reservation helo.AICSARReserved = true + helo:SetDefaultAltitude(self.Altitude or 1500) + + helo:SetDefaultSpeed(self.Speed or 100) + -- Cargo transport assignment to first Huey group. helo:AddOpsTransport(opstransport) @@ -632,6 +911,10 @@ function AICSAR:_InitMission(Pilot,Index) self:__HeloDown(2,Helo,Index) end + local function AICHeloUnloaded(Helo,OpsGroup) + self:__PilotUnloaded(2,Helo,OpsGroup) + end + function helo:OnAfterLoadingDone(From,Event,To) AICPickedUp(helo,helo:GetCargoGroups(),Index) end @@ -640,6 +923,10 @@ function AICSAR:_InitMission(Pilot,Index) AICHeloDead(helo,Index) end + function helo:OnAfterUnloaded(From,Event,To,OpsGroupCargo) + AICHeloUnloaded(helo,OpsGroupCargo) + end + self.helos[Index] = helo return self @@ -650,7 +937,7 @@ end -- @param Wrapper.Group#GROUP Pilot The pilot to be rescued. -- @return #boolean outcome function AICSAR:_CheckInMashZone(Pilot) - self:T(self.lid .. "_CheckQueue") + self:T(self.lid .. "_CheckInMashZone") if Pilot:IsInZone(self.farpzone) then return true else @@ -658,6 +945,26 @@ function AICSAR:_CheckInMashZone(Pilot) end end +--- [User] Set default helo speed. Note - AI might have other ideas. Defaults to 100kn. +-- @param #AICSAR self +-- @param #number Knots Speed in knots. +-- @return #AICSAR self +function AICSAR:SetDefaultSpeed(Knots) + self:T(self.lid .. "SetDefaultSpeed") + self.Speed = Knots or 100 + return self +end + +--- [User] Set default helo altitudeAGL. Note - AI might have other ideas. Defaults to 1500ft. +-- @param #AICSAR self +-- @param #number Feet AGL set in feet. +-- @return #AICSAR self +function AICSAR:SetDefaultAltitude(Feet) + self:T(self.lid .. "SetDefaultAltitude") + self.Altitude = Feet or 1500 + return self +end + --- [Internal] Check helo queue -- @param #AICSAR self -- @return #AICSAR self @@ -694,12 +1001,14 @@ end --- [Internal] Check pilot queue for next mission -- @param #AICSAR self +-- @param Ops.OpsGroup#OPSGROUP OpsGroup -- @return #AICSAR self -function AICSAR:_CheckQueue() +function AICSAR:_CheckQueue(OpsGroup) self:T(self.lid .. "_CheckQueue") for _index, _pilot in pairs(self.pilotqueue) do local classname = _pilot.ClassName and _pilot.ClassName or "NONE" local name = _pilot.GroupName and _pilot.GroupName or "NONE" + local playername = "John Doe" local helocount = self:_CountHelos() --self:T("Looking at " .. classname .. " " .. name) -- find one w/o mission @@ -707,11 +1016,18 @@ function AICSAR:_CheckQueue() local flightgroup = self.helos[_index] -- Ops.FlightGroup#FLIGHTGROUP -- rescued? if self:_CheckInMashZone(_pilot) then - self:T("Pilot" .. _pilot.GroupName .. " rescued!") - _pilot:Destroy(false) + self:T("Pilot" .. _pilot.GroupName .. " rescued!") + if OpsGroup then + OpsGroup:Despawn(10) + else + _pilot:Destroy(true,10) + end self.pilotqueue[_index] = nil self.rescued[_index] = true - self:__PilotRescued(2) + if self.PilotStore:Count() > 0 then + playername = self.PilotStore:Pull() + end + self:__PilotRescued(2,playername) if flightgroup then flightgroup.AICSARReserved = false end @@ -765,7 +1081,7 @@ end -- @return #AICSAR self function AICSAR:onafterStatus(From, Event, To) self:T({From, Event, To}) - self:_CheckQueue() + --self:_CheckQueue() self:_CheckHelos() self:__Status(30) return self @@ -812,6 +1128,12 @@ function AICSAR:onafterPilotDown(From, Event, To, Coordinate, InReach) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) + elseif self.SRSTTSRadio then + if self.SRSOperatorVoice then + self.SRSQ:NewTransmission(text,nil,self.SRSOperator,nil,1) + else + self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) + end end else local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("INITIALNOTOK",self.locale) @@ -826,8 +1148,15 @@ function AICSAR:onafterPilotDown(From, Event, To, Coordinate, InReach) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) + elseif self.SRSTTSRadio then + if self.SRSOperatorVoice then + self.SRSQ:NewTransmission(text,nil,self.SRSOperator,nil,1) + else + self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) + end end end + self:_CheckQueue() return self end @@ -849,7 +1178,9 @@ function AICSAR:onafterPilotKIA(From, Event, To) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) - end + elseif self.SRSTTSRadio then + self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) + end return self end @@ -873,6 +1204,12 @@ function AICSAR:onafterHeloDown(From, Event, To, Helo, Index) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) + elseif self.SRSTTSRadio then + if self.SRSOperatorVoice then + self.SRSQ:NewTransmission(text,nil,self.SRSOperator,nil,1) + else + self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) + end end local findex = 0 local fhname = Helo:GetName() @@ -912,8 +1249,9 @@ end -- @param #string From -- @param #string Event -- @param #string To +-- @param #string PilotName -- @return #AICSAR self -function AICSAR:onafterPilotRescued(From, Event, To) +function AICSAR:onafterPilotRescued(From, Event, To, PilotName) self:T({From, Event, To}) local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("PILOTRESCUED",self.locale) if self.verbose then @@ -925,10 +1263,26 @@ function AICSAR:onafterPilotRescued(From, Event, To) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) + elseif self.SRSTTSRadio then + self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) end return self end +--- [Internal] onafterPilotUnloaded +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.FlightGroup#FLIGHTGROUP Helo +-- @param Ops.OpsGroup#OPSGROUP OpsGroup +-- @return #AICSAR self +function AICSAR:onafterPilotUnloaded(From, Event, To, Helo, OpsGroup) + self:T({From, Event, To}) + self:_CheckQueue(OpsGroup) + return self +end + --- [Internal] onafterPilotPickedUp -- @param #AICSAR self -- @param #string From @@ -950,6 +1304,8 @@ function AICSAR:onafterPilotPickedUp(From, Event, To, Helo, CargoTable, Index) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) + elseif self.SRSTTSRadio then + self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) end local findex = 0 local fhname = Helo:GetName()