--- **Wrapper** - CLIENT wraps DCS Unit objects acting as a __Client__ or __Player__ within a mission. -- -- === -- -- ### Author: **FlightControl** -- ### Contributions: **funkyfranky** -- -- === -- -- @module Wrapper.Client -- @image Wrapper_Client.JPG --- The CLIENT class -- @type CLIENT -- @field #string ClassName Name of the class. -- @field #string ClientName Name of the client. -- @field #string ClientBriefing Briefing. -- @field #function ClientCallBack Callback function. -- @field #table ClientParameters Parameters of the callback function. -- @field #number ClientGroupID Group ID of the client. -- @field #string ClientGroupName Group name. -- @field #boolean ClientAlive Client alive. -- @field #boolean ClientAlive2 Client alive 2. -- @field #table Players Player table. -- @field Core.Point#COORDINATE SpawnCoord Spawn coordinate from the template. -- @extends Wrapper.Unit#UNIT --- Wrapper class of those **Units** defined within the Mission Editor that have the skillset defined as __Client__ or __Player__. -- -- Note that clients are NOT the same as Units, they are NOT necessarily alive. -- The CLIENT class is a wrapper class to handle the DCS Unit objects that have the skillset defined as __Client__ or __Player__: -- -- * Wraps the DCS Unit objects with skill level set to Player or Client. -- * Support all DCS Unit APIs. -- * Enhance with Unit specific APIs not in the DCS Group API set. -- * When player joins Unit, execute alive init logic. -- * Handles messages to players. -- * Manage the "state" of the DCS Unit. -- -- Clients are being used by the @{Tasking.Mission#MISSION} class to follow players and register their successes. -- -- ## CLIENT reference methods -- -- For each DCS Unit having skill level Player or Client, a CLIENT wrapper object (instance) will be created within the global _DATABASE object (an instance of @{Core.Database#DATABASE}). -- This is done at the beginning of the mission (when the mission starts). -- -- The CLIENT class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference -- using the DCS Unit or the DCS UnitName. -- -- Another thing to know is that CLIENT objects do not "contain" the DCS Unit object. -- The CLIENT methods will reference the DCS Unit object by name when it is needed during API execution. -- If the DCS Unit object does not exist or is nil, the CLIENT methods will return nil and log an exception in the DCS.log file. -- -- The CLIENT class provides the following functions to retrieve quickly the relevant CLIENT instance: -- -- * @{#CLIENT.Find}(): Find a CLIENT instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Unit object. -- * @{#CLIENT.FindByName}(): Find a CLIENT instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Unit name. -- -- **IMPORTANT: ONE SHOULD NEVER SANITIZE these CLIENT OBJECT REFERENCES! (make the CLIENT object references nil).** -- -- @field #CLIENT CLIENT = { ClassName = "CLIENT", ClientName = nil, ClientAlive = false, ClientTransport = false, ClientBriefingShown = false, _Menus = {}, _Tasks = {}, Messages = {}, Players = {}, } --- Finds a CLIENT from the _DATABASE using the relevant DCS Unit. -- @param #CLIENT self -- @param DCS#Unit DCSUnit The DCS unit of the client. -- @param #boolean Error Throw an error message. -- @return #CLIENT The CLIENT found in the _DATABASE. function CLIENT:Find(DCSUnit, Error) local ClientName = DCSUnit:getName() local ClientFound = _DATABASE:FindClient( ClientName ) if ClientFound then ClientFound:F( ClientName ) return ClientFound end if not Error then error( "CLIENT not found for: " .. ClientName ) 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. -- @param #CLIENT self -- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor. -- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client. -- @param #boolean Error A flag that indicates whether an error should be raised if the CLIENT cannot be found. By default an error will be raised. -- @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 ) -- -- 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 local ClientFound = _DATABASE:FindClient( ClientName ) if ClientFound then ClientFound:F( { ClientName, ClientBriefing } ) ClientFound:AddBriefing(ClientBriefing) ClientFound.MessageSwitch = true return ClientFound end if not Error then error( "CLIENT not found for: " .. ClientName ) end end --- Transport defines that the Client is a Transport. Transports show cargo. -- @param #CLIENT self -- @param #string ClientName Name of the client unit. -- @return #CLIENT self function CLIENT:Register(ClientName) -- Inherit unit. local self = BASE:Inherit( self, UNIT:Register(ClientName )) -- #CLIENT -- Set client name. self.ClientName = ClientName -- Message switch. self.MessageSwitch = true -- Alive2. self.ClientAlive2 = false return self end --- Transport defines that the Client is a Transport. Transports show cargo. -- @param #CLIENT self -- @return #CLIENT self function CLIENT:Transport() self.ClientTransport = true return self end --- Adds a briefing to a CLIENT when a player joins a mission. -- @param #CLIENT self -- @param #string ClientBriefing is the text defining the Mission briefing. -- @return #CLIENT self function CLIENT:AddBriefing( ClientBriefing ) self.ClientBriefing = ClientBriefing self.ClientBriefingShown = false return self end --- Add player name. -- @param #CLIENT self -- @param #string PlayerName Name of the player. -- @return #CLIENT self function CLIENT:AddPlayer(PlayerName) table.insert(self.Players, PlayerName) return self end --- Get player name(s). -- @param #CLIENT self -- @return #table List of player names or an empty table `{}`. function CLIENT:GetPlayers() return self.Players end --- Get name of player. -- @param #CLIENT self -- @return #string Player name or `nil`. function CLIENT:GetPlayer() if #self.Players>0 then return self.Players[1] end return nil end --- Remove player. -- @param #CLIENT self -- @param #string PlayerName Name of the player. -- @return #CLIENT self function CLIENT:RemovePlayer(PlayerName) for i,playername in pairs(self.Players) do if PlayerName==playername then table.remove(self.Players, i) break end end return self end --- Remove all players. -- @param #CLIENT self -- @return #CLIENT self function CLIENT:RemovePlayers() self.Players={} return self end --- Show the briefing of a CLIENT. -- @param #CLIENT self -- @return #CLIENT self function CLIENT:ShowBriefing() if not self.ClientBriefingShown then self.ClientBriefingShown = true local Briefing = "" if self.ClientBriefing and self.ClientBriefing ~= "" then Briefing = Briefing .. self.ClientBriefing self:Message( Briefing, 60, "Briefing" ) end end return self end --- Show the mission briefing of a MISSION to the CLIENT. -- @param #CLIENT self -- @param #string MissionBriefing -- @return #CLIENT self function CLIENT:ShowMissionBriefing( MissionBriefing ) self:F( { self.ClientName } ) if MissionBriefing then self:Message( MissionBriefing, 60, "Mission Briefing" ) end return self end --- Resets a CLIENT. -- @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 = {} end -- Is Functions --- Checks if the CLIENT is a multi-seated UNIT. -- @param #CLIENT self -- @return #boolean true if multi-seated. function CLIENT:IsMultiSeated() self:F( self.ClientName ) local ClientMultiSeatedTypes = { ["Mi-8MT"] = "Mi-8MT", ["UH-1H"] = "UH-1H", ["P-51B"] = "P-51B" } if self:IsAlive() then local ClientTypeName = self:GetClientGroupUnit():GetTypeName() if ClientMultiSeatedTypes[ClientTypeName] then return true end end return false end --- Checks for a client alive event and calls a function on a continuous basis. -- @param #CLIENT self -- @param #function CallBackFunction Create a function that will be called when a player joins the slot. -- @param ... (Optional) Arguments for callback function as comma separated list. -- @return #CLIENT function CLIENT:Alive( CallBackFunction, ... ) self:F() self.ClientCallBack = CallBackFunction self.ClientParameters = arg self.AliveCheckScheduler = SCHEDULER:New( self, self._AliveCheckScheduler, { "Client Alive " .. self.ClientName }, 0.1, 5, 0.5 ) self.AliveCheckScheduler:NoTrace() return self end -- @param #CLIENT self function CLIENT:_AliveCheckScheduler( SchedulerName ) self:F3( { SchedulerName, self.ClientName, self.ClientAlive2, self.ClientBriefingShown, self.ClientCallBack } ) if self:IsAlive() then if self.ClientAlive2 == false then -- Show briefing. self:ShowBriefing() -- Callback function. if self.ClientCallBack then self:T("Calling Callback function") self.ClientCallBack( self, unpack( self.ClientParameters ) ) end -- Alive. self.ClientAlive2 = true end else if self.ClientAlive2 == true then self.ClientAlive2 = false end end return true end --- Return the DCSGroup of a Client. -- This function is modified to deal with a couple of bugs in DCS 1.5.3 -- @param #CLIENT self -- @return DCS#Group The group of the Client. 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 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 --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!" ) 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 end -- Nothing could be found :( self.ClientGroupID = nil self.ClientGroupName = nil return nil end --- Get the group ID of the client. -- @param #CLIENT self -- @return #number DCS#Group ID. function CLIENT:GetClientGroupID() -- This updates the ID. self:GetDCSGroup() return self.ClientGroupID end --- Get the name of the group of the client. -- @param #CLIENT self -- @return #string function CLIENT:GetClientGroupName() -- This updates the group name. self:GetDCSGroup() return self.ClientGroupName end --- Returns the UNIT of the CLIENT. -- @param #CLIENT self -- @return Wrapper.Unit#UNIT The client UNIT or `nil`. function CLIENT:GetClientGroupUnit() self:F2() 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 end --- Returns the DCSUnit of the CLIENT. -- @param #CLIENT self -- @return DCS#Unit function CLIENT:GetClientGroupDCSUnit() self:F2() local ClientDCSUnit = Unit.getByName( self.ClientName ) if ClientDCSUnit and ClientDCSUnit:isExist() then self:T2( ClientDCSUnit ) return ClientDCSUnit end end --- Evaluates if the CLIENT is a transport. -- @param #CLIENT self -- @return #boolean true is a transport. function CLIENT:IsTransport() 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() 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 if CargoMsg == "" then CargoMsg = "empty" end self:Message( CargoMsg, 15, "Co-Pilot: Cargo Status", 30 ) end --- The main message driver for the CLIENT. -- This function displays various messages to the Player logged into the CLIENT through the DCS World Messaging system. -- @param #CLIENT self -- @param #string Message is the text describing the message. -- @param #number MessageDuration is the duration in seconds that the Message should be displayed. -- @param #string MessageCategory is the category of the message (the title). -- @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 } ) 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 --- [Multi-Player Server] Get UCID from a CLIENT. -- @param #CLIENT self -- @return #string UCID function CLIENT:GetUCID() local PID = NET.GetPlayerIDByName(nil,self:GetPlayerName()) return net.get_player_info(tonumber(PID), 'ucid') end --- [Multi-Player Server] Return a table of attributes from CLIENT. If optional attribute is present, only that value is returned. -- @param #CLIENT self -- @param #string Attribute (Optional) The attribute to obtain. List see below. -- @return #table PlayerInfo or nil if it cannot be found -- @usage -- Returned table holds these attributes: -- -- 'id' : player ID -- 'name' : player name -- 'side' : 0 - spectators, 1 - red, 2 - blue -- 'slot' : slot ID of the player or -- 'ping' : ping of the player in ms -- 'ipaddr': IP address of the player, SERVER ONLY -- 'ucid' : Unique Client Identifier, SERVER ONLY -- function CLIENT:GetPlayerInfo(Attribute) local PID = NET.GetPlayerIDByName(nil,self:GetPlayerName()) if PID then return net.get_player_info(tonumber(PID), Attribute) else return nil end end