diff --git a/Moose Development/Moose/Functional/AICSAR.lua b/Moose Development/Moose/Functional/AICSAR.lua index b1ee470ca..c9ec13298 100644 --- a/Moose Development/Moose/Functional/AICSAR.lua +++ b/Moose Development/Moose/Functional/AICSAR.lua @@ -52,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 @@ -77,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")) @@ -186,7 +191,7 @@ -- @field #AICSAR AICSAR = { ClassName = "AICSAR", - version = "0.1.9", + version = "0.1.14", lid = "", coalition = coalition.side.BLUE, template = "", @@ -226,6 +231,11 @@ AICSAR = { SRSPilotVoice = false, SRSOperator = nil, SRSOperatorVoice = false, + PilotStore = nil, + Speed = 100, + Altitude = 1500, + UseEventEject = false, + Delay = 100, } -- TODO Messages @@ -337,6 +347,8 @@ 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 @@ -369,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") @@ -384,7 +399,8 @@ function AICSAR:New(Alias,Coalition,Pilottemplate,Helotemplate,FARP,MASHZone) self:AddTransition("*", "HeloDown", "*") -- Helo dead 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)) @@ -438,7 +454,8 @@ 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 @@ -648,11 +665,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? @@ -662,12 +771,14 @@ 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()) @@ -765,10 +876,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) @@ -815,6 +931,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 @@ -858,6 +994,7 @@ function AICSAR:_CheckQueue(OpsGroup) 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 @@ -873,7 +1010,10 @@ function AICSAR:_CheckQueue(OpsGroup) 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 @@ -1095,8 +1235,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 diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index db2afa9c0..9524e67b0 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -22,7 +22,7 @@ -- @module Ops.CTLD -- @image OPS_CTLD.jpg --- Last Update Jan 2023 +-- Last Update Feb 2023 do @@ -1196,7 +1196,7 @@ CTLD.UnitTypes = { --- CTLD class version. -- @field #string version -CTLD.version="1.0.30" +CTLD.version="1.0.31" --- Instantiate a new CTLD. -- @param #CTLD self @@ -2609,7 +2609,7 @@ function CTLD:_LoadCratesNearby(Group, Unit) crateind = _crate:GetID() end else - if not _crate:HasMoved() and _crate:WasDropped() and _crate:GetID() > crateind then + if not _crate:HasMoved() and not _crate:WasDropped() and _crate:GetID() > crateind then crateind = _crate:GetID() end end diff --git a/Moose Development/Moose/Wrapper/Client.lua b/Moose Development/Moose/Wrapper/Client.lua index 12f2baa13..01dce4828 100644 --- a/Moose Development/Moose/Wrapper/Client.lua +++ b/Moose Development/Moose/Wrapper/Client.lua @@ -62,15 +62,15 @@ -- -- @field #CLIENT CLIENT = { - ClassName = "CLIENT", - ClientName = nil, - ClientAlive = false, - ClientTransport = false, - ClientBriefingShown = false, - _Menus = {}, - _Tasks = {}, - Messages = {}, - Players = {}, + ClassName = "CLIENT", + ClientName = nil, + ClientAlive = false, + ClientTransport = false, + ClientBriefingShown = false, + _Menus = {}, + _Tasks = {}, + Messages = {}, + Players = {}, } @@ -95,6 +95,22 @@ function CLIENT:Find(DCSUnit, Error) end end +--- Finds a CLIENT from the _DATABASE using the relevant player name. +-- @param #CLIENT self +-- @param #string Name Name of the player +-- @return #CLIENT or nil if not found +function CLIENT:FindByPlayerName(Name) + + local foundclient = nil + _DATABASE:ForEachClient( + function(client) + if client:GetPlayerName() == Name then + foundclient = client + end + end + ) + return foundclient +end --- Finds a CLIENT from the _DATABASE using the relevant Client Unit Name. -- As an optional parameter, a briefing text can be given also. @@ -105,13 +121,13 @@ end -- @return #CLIENT -- @usage -- -- Create new Clients. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- Mission:AddGoal( DeploySA6TroopsGoal ) +-- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) +-- Mission:AddGoal( DeploySA6TroopsGoal ) -- --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) function CLIENT:FindByName( ClientName, ClientBriefing, Error ) -- Client @@ -124,7 +140,7 @@ function CLIENT:FindByName( ClientName, ClientBriefing, Error ) ClientFound.MessageSwitch = true - return ClientFound + return ClientFound end if not Error then @@ -262,8 +278,8 @@ end -- @param #CLIENT self -- @param #string ClientName Name of the Group as defined within the Mission Editor. The Group must have a Unit with the type Client. function CLIENT:Reset( ClientName ) - self:F() - self._Menus = {} + self:F() + self._Menus = {} end -- Is Functions @@ -347,85 +363,85 @@ function CLIENT:GetDCSGroup() self:F3() -- local ClientData = Group.getByName( self.ClientName ) --- if ClientData and ClientData:isExist() then --- self:T( self.ClientName .. " : group found!" ) --- return ClientData --- else --- return nil --- end +-- if ClientData and ClientData:isExist() then +-- self:T( self.ClientName .. " : group found!" ) +-- return ClientData +-- else +-- return nil +-- end local ClientUnit = Unit.getByName( self.ClientName ) - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } - - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - self:T3( { "CoalitionData:", CoalitionData } ) - for UnitId, UnitData in pairs( CoalitionData ) do - self:T3( { "UnitData:", UnitData } ) - if UnitData and UnitData:isExist() then + local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } + + for CoalitionId, CoalitionData in pairs( CoalitionsData ) do + self:T3( { "CoalitionData:", CoalitionData } ) + for UnitId, UnitData in pairs( CoalitionData ) do + self:T3( { "UnitData:", UnitData } ) + if UnitData and UnitData:isExist() then --self:F(self.ClientName) if ClientUnit then - local ClientGroup = ClientUnit:getGroup() - - if ClientGroup then - self:T3( "ClientGroup = " .. self.ClientName ) - - if ClientGroup:isExist() and UnitData:getGroup():isExist() then - - if ClientGroup:getID() == UnitData:getGroup():getID() then - self:T3( "Normal logic" ) - self:T3( self.ClientName .. " : group found!" ) + local ClientGroup = ClientUnit:getGroup() + + if ClientGroup then + self:T3( "ClientGroup = " .. self.ClientName ) + + if ClientGroup:isExist() and UnitData:getGroup():isExist() then + + if ClientGroup:getID() == UnitData:getGroup():getID() then + self:T3( "Normal logic" ) + self:T3( self.ClientName .. " : group found!" ) self.ClientGroupID = ClientGroup:getID() - self.ClientGroupName = ClientGroup:getName() - return ClientGroup - end - - else - - -- Now we need to resolve the bugs in DCS 1.5 ... - -- Consult the database for the units of the Client Group. (ClientGroup:getUnits() returns nil) - self:T3( "Bug 1.5 logic" ) - - local ClientGroupTemplate = _DATABASE.Templates.Units[self.ClientName].GroupTemplate - - self.ClientGroupID = ClientGroupTemplate.groupId - - self.ClientGroupName = _DATABASE.Templates.Units[self.ClientName].GroupName - - self:T3( self.ClientName .. " : group found in bug 1.5 resolvement logic!" ) - return ClientGroup - - end - -- else - -- error( "Client " .. self.ClientName .. " not found!" ) - end - else - --self:F( { "Client not found!", self.ClientName } ) - end - end - end - end - - -- For non player clients - if ClientUnit then - local ClientGroup = ClientUnit:getGroup() - if ClientGroup then - self:T3( "ClientGroup = " .. self.ClientName ) - if ClientGroup:isExist() then - self:T3( "Normal logic" ) - self:T3( self.ClientName .. " : group found!" ) - return ClientGroup - end - end + self.ClientGroupName = ClientGroup:getName() + return ClientGroup + end + + else + + -- Now we need to resolve the bugs in DCS 1.5 ... + -- Consult the database for the units of the Client Group. (ClientGroup:getUnits() returns nil) + self:T3( "Bug 1.5 logic" ) + + local ClientGroupTemplate = _DATABASE.Templates.Units[self.ClientName].GroupTemplate + + self.ClientGroupID = ClientGroupTemplate.groupId + + self.ClientGroupName = _DATABASE.Templates.Units[self.ClientName].GroupName + + self:T3( self.ClientName .. " : group found in bug 1.5 resolvement logic!" ) + return ClientGroup + + end + -- else + -- error( "Client " .. self.ClientName .. " not found!" ) + end + else + --self:F( { "Client not found!", self.ClientName } ) + end + end + end end - - -- Nothing could be found :( - self.ClientGroupID = nil - self.ClientGroupName = nil - - return nil + + -- For non player clients + if ClientUnit then + local ClientGroup = ClientUnit:getGroup() + if ClientGroup then + self:T3( "ClientGroup = " .. self.ClientName ) + if ClientGroup:isExist() then + self:T3( "Normal logic" ) + self:T3( self.ClientName .. " : group found!" ) + return ClientGroup + end + end + end + + -- Nothing could be found :( + self.ClientGroupID = nil + self.ClientGroupName = nil + + return nil end @@ -437,7 +453,7 @@ function CLIENT:GetClientGroupID() -- This updates the ID. self:GetDCSGroup() - return self.ClientGroupID + return self.ClientGroupID end @@ -449,7 +465,7 @@ function CLIENT:GetClientGroupName() -- This updates the group name. self:GetDCSGroup() - return self.ClientGroupName + return self.ClientGroupName end --- Returns the UNIT of the CLIENT. @@ -458,23 +474,23 @@ end function CLIENT:GetClientGroupUnit() self:F2() - local ClientDCSUnit = Unit.getByName( self.ClientName ) + local ClientDCSUnit = Unit.getByName( self.ClientName ) self:T( self.ClientDCSUnit ) - if ClientDCSUnit and ClientDCSUnit:isExist() then - local ClientUnit=_DATABASE:FindUnit( self.ClientName ) - return ClientUnit - end - - return nil + if ClientDCSUnit and ClientDCSUnit:isExist() then + local ClientUnit=_DATABASE:FindUnit( self.ClientName ) + return ClientUnit + end + + return nil end --- Returns the DCSUnit of the CLIENT. -- @param #CLIENT self -- @return DCS#Unit function CLIENT:GetClientGroupDCSUnit() - self:F2() + self:F2() local ClientDCSUnit = Unit.getByName( self.ClientName ) @@ -489,29 +505,29 @@ end -- @param #CLIENT self -- @return #boolean true is a transport. function CLIENT:IsTransport() - self:F() - return self.ClientTransport + self:F() + return self.ClientTransport end --- Shows the @{AI.AI_Cargo#CARGO} contained within the CLIENT to the player as a message. -- The @{AI.AI_Cargo#CARGO} is shown using the @{Core.Message#MESSAGE} distribution system. -- @param #CLIENT self function CLIENT:ShowCargo() - self:F() + self:F() - local CargoMsg = "" + local CargoMsg = "" - for CargoName, Cargo in pairs( CARGOS ) do - if self == Cargo:IsLoadedInClient() then - CargoMsg = CargoMsg .. Cargo.CargoName .. " Type:" .. Cargo.CargoType .. " Weight: " .. Cargo.CargoWeight .. "\n" - end - end + for CargoName, Cargo in pairs( CARGOS ) do + if self == Cargo:IsLoadedInClient() then + CargoMsg = CargoMsg .. Cargo.CargoName .. " Type:" .. Cargo.CargoType .. " Weight: " .. Cargo.CargoWeight .. "\n" + end + end - if CargoMsg == "" then - CargoMsg = "empty" - end + if CargoMsg == "" then + CargoMsg = "empty" + end - self:Message( CargoMsg, 15, "Co-Pilot: Cargo Status", 30 ) + self:Message( CargoMsg, 15, "Co-Pilot: Cargo Status", 30 ) end @@ -526,39 +542,39 @@ end -- @param #number MessageInterval is the interval in seconds between the display of the @{Core.Message#MESSAGE} when the CLIENT is in the air. -- @param #string MessageID is the identifier of the message when displayed with intervals. function CLIENT:Message( Message, MessageDuration, MessageCategory, MessageInterval, MessageID ) - self:F( { Message, MessageDuration, MessageCategory, MessageInterval } ) + self:F( { Message, MessageDuration, MessageCategory, MessageInterval } ) - if self.MessageSwitch == true then - if MessageCategory == nil then - MessageCategory = "Messages" - end - if MessageID ~= nil then - if self.Messages[MessageID] == nil then - self.Messages[MessageID] = {} - self.Messages[MessageID].MessageId = MessageID - self.Messages[MessageID].MessageTime = timer.getTime() - self.Messages[MessageID].MessageDuration = MessageDuration - if MessageInterval == nil then - self.Messages[MessageID].MessageInterval = 600 - else - self.Messages[MessageID].MessageInterval = MessageInterval - end - MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) - else - if self:GetClientGroupDCSUnit() and not self:GetClientGroupDCSUnit():inAir() then - if timer.getTime() - self.Messages[MessageID].MessageTime >= self.Messages[MessageID].MessageDuration + 10 then - MESSAGE:New( Message, MessageDuration , MessageCategory):ToClient( self ) - self.Messages[MessageID].MessageTime = timer.getTime() - end - else - if timer.getTime() - self.Messages[MessageID].MessageTime >= self.Messages[MessageID].MessageDuration + self.Messages[MessageID].MessageInterval then - MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) - self.Messages[MessageID].MessageTime = timer.getTime() - end - end - end - else + if self.MessageSwitch == true then + if MessageCategory == nil then + MessageCategory = "Messages" + end + if MessageID ~= nil then + if self.Messages[MessageID] == nil then + self.Messages[MessageID] = {} + self.Messages[MessageID].MessageId = MessageID + self.Messages[MessageID].MessageTime = timer.getTime() + self.Messages[MessageID].MessageDuration = MessageDuration + if MessageInterval == nil then + self.Messages[MessageID].MessageInterval = 600 + else + self.Messages[MessageID].MessageInterval = MessageInterval + end + MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) + else + if self:GetClientGroupDCSUnit() and not self:GetClientGroupDCSUnit():inAir() then + if timer.getTime() - self.Messages[MessageID].MessageTime >= self.Messages[MessageID].MessageDuration + 10 then + MESSAGE:New( Message, MessageDuration , MessageCategory):ToClient( self ) + self.Messages[MessageID].MessageTime = timer.getTime() + end + else + if timer.getTime() - self.Messages[MessageID].MessageTime >= self.Messages[MessageID].MessageDuration + self.Messages[MessageID].MessageInterval then + MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) + self.Messages[MessageID].MessageTime = timer.getTime() + end + end + end + else MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) end - end + end end diff --git a/Moose Development/Moose/Wrapper/Net.lua b/Moose Development/Moose/Wrapper/Net.lua index d2ac28285..8f394eee7 100644 --- a/Moose Development/Moose/Wrapper/Net.lua +++ b/Moose Development/Moose/Wrapper/Net.lua @@ -1,11 +1,12 @@ --- **Wrapper** - DCS net functions. -- --- Encapsules **multiplayer** environment scripting functions from [net](https://wiki.hoggitworld.com/view/DCS_singleton_net) +-- Encapsules **multiplayer server** environment scripting functions from [net](https://wiki.hoggitworld.com/view/DCS_singleton_net) -- -- === -- -- ### Author: **applevangelist** --- +-- # Last Update Feb 2023 +-- -- === -- -- @module Wrapper.Net @@ -22,17 +23,31 @@ do -- @field #table KnownPilots -- @field #string BlockMessage -- @field #string UnblockMessage +-- @field #table BlockedUCIDs +-- @field #table BlockedSlots +-- @field #table BlockedSides -- @extends Core.Fsm#FSM +--- +-- @type NET.PlayerData +-- @field #string name +-- @field #string ucid +-- @field #number id +-- @field #number side +-- @field #number slot + --- Encapsules multiplayer environment scripting functions from [net](https://wiki.hoggitworld.com/view/DCS_singleton_net) -- with some added FSM functions and options to block/unblock players in MP environments. -- -- @field #NET NET = { ClassName = "NET", - Version = "0.0.3", + Version = "0.1.0", BlockTime = 600, BlockedPilots = {}, + BlockedUCIDs = {}, + BlockedSides = {}, + BlockedSlots = {}, KnownPilots = {}, BlockMessage = nil, UnblockMessage = nil, @@ -52,26 +67,20 @@ function NET:New() self:SetBlockMessage() self:SetUnblockMessage() - self:HandleEvent(EVENTS.PlayerEnterUnit,self._EventHandler) - self:HandleEvent(EVENTS.PlayerEnterAircraft,self._EventHandler) - self:HandleEvent(EVENTS.PlayerLeaveUnit,self._EventHandler) - self:HandleEvent(EVENTS.PilotDead,self._EventHandler) - self:HandleEvent(EVENTS.Ejection,self._EventHandler) - self:HandleEvent(EVENTS.Crash,self._EventHandler) - self:HandleEvent(EVENTS.SelfKillPilot,self._EventHandler) - -- Start State. - self:SetStartState("Running") + self:SetStartState("Stopped") -- Add FSM transitions. -- From State --> Event --> To State - self:AddTransition("*", "Run", "Running") -- Start FSM. + self:AddTransition("Stopped", "Run", "Running") -- Start FSM. self:AddTransition("*", "PlayerJoined", "*") self:AddTransition("*", "PlayerLeft", "*") self:AddTransition("*", "PlayerDied", "*") self:AddTransition("*", "PlayerEjected", "*") self:AddTransition("*", "PlayerBlocked", "*") self:AddTransition("*", "PlayerUnblocked", "*") + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "Stop", "Stopped") self.lid = string.format("NET %s | ",self.Version) @@ -91,7 +100,7 @@ function NET:New() -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. - -- @param Wrapper.Unit#UNIT Client Unit Object. + -- @param Wrapper.Unit#UNIT Client Unit Object, might be nil. -- @param #string Name Name of leaving Pilot. -- @return #NET self @@ -101,7 +110,7 @@ function NET:New() -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. - -- @param Wrapper.Unit#UNIT Client Unit Object. + -- @param Wrapper.Unit#UNIT Client Unit Object, might be nil. -- @param #string Name Name of leaving Pilot. -- @return #NET self @@ -111,7 +120,7 @@ function NET:New() -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. - -- @param Wrapper.Unit#UNIT Client Unit Object. + -- @param Wrapper.Unit#UNIT Client Unit Object, might be nil. -- @param #string Name Name of dead Pilot. -- @return #NET self @@ -121,7 +130,7 @@ function NET:New() -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. - -- @param Wrapper.Client#CLIENT Client Client Object. + -- @param Wrapper.Client#CLIENT Client Client Object, might be nil. -- @param #string Name Name of blocked Pilot. -- @param #number Seconds Blocked for this number of seconds -- @return #NET self @@ -132,13 +141,49 @@ function NET:New() -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. - -- @param Wrapper.Client#CLIENT Client Client Object. + -- @param Wrapper.Client#CLIENT Client Client Object, might be nil. -- @param #string Name Name of unblocked Pilot. -- @return #NET self + self:Run() + return self end +--- [Internal] Check any blockers +-- @param #NET self +-- @param #string UCID +-- @param #string Name +-- @param #number PlayerID +-- @param #number PlayerSide +-- @param #string PlayerSlot +-- @return #boolean IsBlocked +function NET:IsAnyBlocked(UCID,Name,PlayerID,PlayerSide,PlayerSlot) + local blocked = false + local TNow = timer.getTime() + -- UCID + if UCID and self.BlockedUCIDs[UCID] and TNow < self.BlockedUCIDs[UCID] then + return true + end + -- ID/Name + if PlayerID and not Name then + Name = self:GetPlayerIDByName(Name) + end + -- Name + if Name and self.BlockedPilots[Name] and TNow < self.BlockedPilots[Name] then + return true + end + -- Side + if PlayerSide and self.BlockedSides[PlayerSide] and TNow < self.BlockedSides[PlayerSide] then + return true + end + -- Slot + if PlayerSlot and self.BlockedSlots[PlayerSlot] and TNow < self.BlockedSlots[PlayerSlot] then + return true + end + return blocked +end + --- [Internal] Event Handler -- @param #NET self -- @param Core.Event#EVENTDATA EventData @@ -148,42 +193,63 @@ function NET:_EventHandler(EventData) self:T2({Event = EventData.id}) local data = EventData -- Core.Event#EVENTDATA EventData if data.id and data.IniUnit and (data.IniPlayerName or data.IniUnit:GetPlayerName()) then - -- Get PlayerName + + -- Get Player Data local name = data.IniPlayerName and data.IniPlayerName or data.IniUnit:GetPlayerName() - self:T(self.lid.."Event for: "..name) + local ucid = self:GetPlayerUCID(nil,name) + local PlayerID = self:GetPlayerIDByName(name) or "none" + local PlayerSide, PlayerSlot = self:GetSlot(data.IniUnit) + local TNow = timer.getTime() + + self:T(self.lid.."Event for: "..name.." | UCID: "..ucid) + -- Joining if data.id == EVENTS.PlayerEnterUnit or data.id == EVENTS.PlayerEnterAircraft then - -- Check for known pilots - local TNow = timer.getTime() - if self.BlockedPilots[name] and TNow < self.BlockedPilots[name] then + self:T(self.lid.."Pilot Joining: "..name.." | UCID: "..ucid) + -- Check for blockages + local blocked = self:IsAnyBlocked(ucid,name,PlayerID,PlayerSide,PlayerSlot) + + if blocked and PlayerID and tonumber(PlayerID) ~= 1 then -- block pilot - self:ReturnToSpectators(data.IniUnit) + local outcome = net.force_player_slot(tonumber(PlayerID), 0, '' ) else - self.KnownPilots[name] = true - self.BlockedPilots[name] = nil + self.KnownPilots[name] = { + name = name, + ucid = ucid, + id = PlayerID, + side = PlayerSide, + slot = PlayerSlot, + } self:__PlayerJoined(1,data.IniUnit,name) return self end end + -- Leaving if data.id == EVENTS.PlayerLeaveUnit and self.KnownPilots[name] then + self:T(self.lid.."Pilot Leaving: "..name.." | UCID: "..ucid) self:__PlayerLeft(1,data.IniUnit,name) self.KnownPilots[name] = false return self end + -- Ejected if data.id == EVENTS.Ejection and self.KnownPilots[name] then + self:T(self.lid.."Pilot Ejecting: "..name.." | UCID: "..ucid) self:__PlayerEjected(1,data.IniUnit,name) self.KnownPilots[name] = false return self end + -- Dead, Crash, Suicide if (data.id == EVENTS.PilotDead or data.id == EVENTS.SelfKillPilot or data.id == EVENTS.Crash) and self.KnownPilots[name] then + self:T(self.lid.."Pilot Dead: "..name.." | UCID: "..ucid) self:__PlayerDied(1,data.IniUnit,name) self.KnownPilots[name] = false return self end end + return self end @@ -195,25 +261,133 @@ end -- @param #string Message (optional) Message to be sent via chat. -- @return #NET self function NET:BlockPlayer(Client,PlayerName,Seconds,Message) - local name - if Client then - name = CLIENT:GetPlayerName() + self:T({PlayerName,Seconds,Message}) + local name = PlayerName + if Client and (not PlayerName) then + name = Client:GetPlayerName() elseif PlayerName then name = PlayerName else - self:F(self.lid.."Block: No PlayerName given or not found!") + self:F(self.lid.."Block: No Client or PlayerName given or nothing found!") return self end + local ucid = self:GetPlayerUCID(Client,name) local addon = Seconds or self.BlockTime self.BlockedPilots[name] = timer.getTime()+addon + self.BlockedUCIDs[ucid] = timer.getTime()+addon local message = Message or self.BlockMessage - if Client then - self:SendChatToPlayer(message,Client) + if name then + self:SendChatToPlayer(message,name) else self:SendChat(name..": "..message) end self:__PlayerBlocked(1,Client,name,Seconds) - self:ReturnToSpectators(Client) + local PlayerID = self:GetPlayerIDByName(name) + if PlayerID and tonumber(PlayerID) ~= 1 then + local outcome = net.force_player_slot(tonumber(PlayerID), 0, '' ) + end + return self +end + +--- Block a SET_CLIENT of players +-- @param #NET self +-- @param Core.Set#SET_CLIENT PlayerSet The SET to block. +-- @param #number Seconds Seconds (optional) Number of seconds the player has to wait before rejoining. +-- @param #string Message (optional) Message to be sent via chat. +-- @return #NET self +function NET:BlockPlayerSet(PlayerSet,Seconds,Message) + self:T({PlayerSet.Set,Seconds,Message}) + local addon = Seconds or self.BlockTime + local message = Message or self.BlockMessage + for _,_client in pairs(PlayerSet.Set) do + local name = _client:GetPlayerName() + self:BlockPlayer(_client,name,addon,message) + end + return self +end + +--- Unblock a SET_CLIENT of players +-- @param #NET self +-- @param Core.Set#SET_CLIENT PlayerSet The SET to unblock. +-- @param #string Message (optional) Message to be sent via chat. +-- @return #NET self +function NET:UnblockPlayerSet(PlayerSet,Message) + self:T({PlayerSet.Set,Seconds,Message}) + local message = Message or self.UnblockMessage + for _,_client in pairs(PlayerSet.Set) do + local name = _client:GetPlayerName() + self:UnblockPlayer(_client,name,message) + end + return self +end + +--- Block a specific UCID of a player, does NOT automatically kick the player with the UCID if already joined. +-- @param #NET self +-- @param #string ucid +-- @param #number Seconds Seconds (optional) Number of seconds the player has to wait before rejoining. +-- @return #NET self +function NET:BlockUCID(ucid,Seconds) + self:T({ucid,Seconds}) + local addon = Seconds or self.BlockTime + self.BlockedUCIDs[ucid] = timer.getTime()+addon + return self +end + +--- Unblock a specific UCID of a player +-- @param #NET self +-- @param #string ucid +-- @return #NET self +function NET:UnblockUCID(ucid) + self:T({ucid}) + self.BlockedUCIDs[ucid] = nil + return self +end + +--- Block a specific coalition side, does NOT automatically kick all players of that side or kick out joined players +-- @param #NET self +-- @param #number side The side to block - 1 : Red, 2 : Blue +-- @param #number Seconds Seconds (optional) Number of seconds the player has to wait before rejoining. +-- @return #NET self +function NET:BlockSide(Side,Seconds) + self:T({Side,Seconds}) + local addon = Seconds or self.BlockTime + if Side == 1 or Side == 2 then + self.BlockedSides[Side] = timer.getTime()+addon + end + return self +end + +--- Unblock a specific coalition side. Does NOT unblock specifically blocked playernames or UCIDs. +-- @param #number side The side to block - 1 : Red, 2 : Blue +-- @param #number Seconds Seconds (optional) Number of seconds the player has to wait before rejoining. +-- @return #NET self +function NET:UnblockSide(Side,Seconds) + self:T({Side,Seconds}) + local addon = Seconds or self.BlockTime + if Side == 1 or Side == 2 then + self.BlockedSides[Side] = nil + end + return self +end + +--- Block a specific player slot, does NOT automatically kick a player in that slot or kick out joined players +-- @param #NET self +-- @param #string slot The slot to block +-- @param #number Seconds Seconds (optional) Number of seconds the player has to wait before rejoining. +-- @return #NET self +function NET:BlockSlot(Slot,Seconds) + self:T({Slot,Seconds}) + local addon = Seconds or self.BlockTime + self.BlockedSlots[Slot] = timer.getTime()+addon + return self +end + +--- Unblock a specific slot. +-- @param #string slot The slot to block +-- @return #NET self +function NET:UnblockSlot(Slot) + self:T({Slot}) + self.BlockedSlots[Slot] = nil return self end @@ -224,19 +398,21 @@ end -- @param #string Message (optional) Message to be sent via chat. -- @return #NET self function NET:UnblockPlayer(Client,PlayerName,Message) - local name + local name = PlayerName if Client then - name = CLIENT:GetPlayerName() + name = Client:GetPlayerName() elseif PlayerName then name = PlayerName else self:F(self.lid.."Unblock: No PlayerName given or not found!") return self end + local ucid = self:GetPlayerUCID(Client,name) self.BlockedPilots[name] = nil + self.BlockedUCIDs[ucid] = nil local message = Message or self.UnblockMessage - if Client then - self:SendChatToPlayer(message,Client) + if name then + self:SendChatToPlayer(message,name) else self:SendChat(name..": "..message) end @@ -244,16 +420,28 @@ function NET:UnblockPlayer(Client,PlayerName,Message) return self end +--- Set block chat message. +-- @param #NET self +-- @param #string Text The message +-- @return #NET self function NET:SetBlockMessage(Text) self.BlockMessage = Text or "You are blocked from joining. Wait time is: "..self.BlockTime.." seconds!" return self end +--- Set block time in seconds. +-- @param #NET self +-- @param #number Seconds Numnber of seconds this block will last. Defaults to 600. +-- @return #NET self function NET:SetBlockTime(Seconds) self.BlockTime = Seconds or 600 return self end +--- Set unblock chat message. +-- @param #NET self +-- @param #string Text The message +-- @return #NET self function NET:SetUnblockMessage(Text) self.UnblockMessage = Text or "You are unblocked now and can join again." return self @@ -275,10 +463,13 @@ end -- @param #NET self -- @param #string Name The player name whose ID to find -- @return #number PlayerID or nil -function NET:GetPlayerIdByName(Name) +function NET:GetPlayerIDByName(Name) + if not Name then return nil end local playerList = self:GetPlayerList() + self:T({playerList}) for i=1,#playerList do local playerName = net.get_name(i) + self:T({playerName}) if playerName == Name then return playerList[i] end @@ -291,18 +482,22 @@ end -- @param Wrapper.Client#CLIENT Client The client -- @return #number PlayerID or nil function NET:GetPlayerIDFromClient(Client) - local name = Client:GetPlayerName() - local id = self:GetPlayerIdByName(name) - return id + if Client then + local name = Client:GetPlayerName() + local id = self:GetPlayerIDByName(name) + return id + else + return nil + end end ---- Send chat message to a specific player. +--- Send chat message to a specific player using the CLIENT object. -- @param #NET self -- @param #string Message The text message -- @param Wrapper.Client#CLIENT ToClient Client receiving the message -- @param Wrapper.Client#CLIENT FromClient (Optional) Client sending the message -- @return #NET self -function NET:SendChatToPlayer(Message, ToClient, FromClient) +function NET:SendChatToClient(Message, ToClient, FromClient) local PlayerId = self:GetPlayerIDFromClient(ToClient) local FromId = self:GetPlayerIDFromClient(FromClient) if Message and PlayerId and FromId then @@ -313,6 +508,23 @@ function NET:SendChatToPlayer(Message, ToClient, FromClient) return self end +--- Send chat message to a specific player using the player name +-- @param #NET self +-- @param #string Message The text message +-- @param #string ToPlayer Player receiving the message +-- @param #string FromPlayer(Optional) Player sending the message +-- @return #NET self +function NET:SendChatToPlayer(Message, ToPlayer, FromPlayer) + local PlayerId = self:GetPlayerIDByName(ToPlayer) + local FromId = self:GetPlayerIDByName(FromPlayer) + if Message and PlayerId and FromId then + net.send_chat_to(Message, tonumber(PlayerId) , tonumber(FromId)) + elseif Message and PlayerId then + net.send_chat_to(Message, tonumber(PlayerId)) + end + return self +end + --- Load a specific mission. -- @param #NET self -- @param #string Path and Mission @@ -384,6 +596,25 @@ function NET:GetPlayerInfo(Client,Attribute) end end + +--- Get player UCID from player CLIENT object or player name. Provide either one. +-- @param #NET self +-- @param Wrapper.Client#CLIENT Client The client object to be used. +-- @param #string Name Player name to be used. +-- @return #boolean success +function NET:GetPlayerUCID(Client,Name) + local PlayerID = nil + if Client then + PlayerID = self:GetPlayerIDFromClient(Client) + elseif Name then + PlayerID = self:GetPlayerIDByName(Name) + else + self:E(self.lid.."Neither client nor name provided!") + end + local ucid = net.get_player_info(tonumber(PlayerID), 'ucid') + return ucid +end + --- Kicks a player from the server. Can display a message to the user. -- @param #NET self -- @param Wrapper.Client#CLIENT Client The client @@ -427,7 +658,7 @@ function NET:GetPlayerStatistic(Client,StatisticID) end end ---- Return the name of a given client. Same a CLIENT:GetPlayerName(). +--- Return the name of a given client. Effectively the same as CLIENT:GetPlayerName(). -- @param #NET self -- @param Wrapper.Client#CLIENT Client The client -- @return #string Name or nil if not obtainable @@ -463,8 +694,8 @@ end -- @return #boolean Success function NET:ForceSlot(Client,SideID,SlotID) local PlayerID = self:GetPlayerIDFromClient(Client) - if PlayerID then - return net.force_player_slot(tonumber(PlayerID), SideID, SlotID ) + if PlayerID and tonumber(PlayerID) ~= 1 then + return net.force_player_slot(tonumber(PlayerID), SideID, SlotID or '' ) else return false end @@ -516,6 +747,86 @@ function NET:Log(Message) return self end +--- Get some data of pilots who have currently joined +-- @param #NET self +-- @param Wrapper.Client#CLIENT Client Provide either the client object whose data to find **or** +-- @param #string Name The player name whose data to find +-- @return #table Table of #NET.PlayerData or nil if not found +function NET:GetKnownPilotData(Client,Name) + local name = Name + if Client and not Name then + name = Client:GetPlayerName() + end + if name then + return self.KnownPilots[name] + else + return nil + end +end + +--- Status - housekeeping +-- @param #NET self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #NET self +function NET:onafterStatus(From,Event,To) + self:T({From,Event,To}) + + local function HouseHold(tavolo) + local TNow = timer.getTime() + for _,entry in pairs (tavolo) do + if entry >= TNow then entry = nil end + end + end + + HouseHold(self.BlockedPilots) + HouseHold(self.BlockedSides) + HouseHold(self.BlockedSlots) + HouseHold(self.BlockedUCIDs) + + if self:Is("Running") then + self:__Status(-60) + end + + return self +end + +--- Stop the event functions +-- @param #NET self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #NET self +function NET:onafterRun(From,Event,To) + self:T({From,Event,To}) + self:HandleEvent(EVENTS.PlayerEnterUnit,self._EventHandler) + self:HandleEvent(EVENTS.PlayerEnterAircraft,self._EventHandler) + self:HandleEvent(EVENTS.PlayerLeaveUnit,self._EventHandler) + self:HandleEvent(EVENTS.PilotDead,self._EventHandler) + self:HandleEvent(EVENTS.Ejection,self._EventHandler) + self:HandleEvent(EVENTS.Crash,self._EventHandler) + self:HandleEvent(EVENTS.SelfKillPilot,self._EventHandler) + self:__Status(-30) +end + +--- Stop the event functions +-- @param #NET self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #NET self +function NET:onafterStop(From,Event,To) + self:T({From,Event,To}) + self:UnHandleEvent(EVENTS.PlayerEnterUnit) + self:UnHandleEvent(EVENTS.PlayerEnterAircraft) + self:UnHandleEvent(EVENTS.PlayerLeaveUnit) + self:UnHandleEvent(EVENTS.PilotDead) + self:UnHandleEvent(EVENTS.Ejection) + self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.SelfKillPilot) + return self +end ------------------------------------------------------------------------------- -- End of NET -------------------------------------------------------------------------------