env.info("DcssbMatchManager - loading script") --- --- This Lua script defines the DcssbMatchManager class, which is designed to manage player participation in matches within a DCS World mission. --- It interacts with a hypothetical "DCSBot" (likely a Discord bot or similar) to register players into matches. --- The script supports adding players automatically after a timeout, or when they enter a designated trigger zone. --- It leverages DCS World's event system to track player activities, specifically focusing on when players enter units. --- --- Key Components: --- * DcssbMatchManager Class: The core of the script. It handles match creation, player registration, and event processing. --- * DcssbMatchManager:new(): Constructor for the DcssbMatchManager class. Initializes a new match manager instance. --- * DcssbMatchManager:setName(), DcssbMatchManager:setMatchName(), DcssbMatchManager:setCoalition(), DcssbMatchManager:setTriggerZone(), DcssbMatchManager:setTimeout(): Setter methods to configure the match manager's properties (name, match name, coalition, trigger zone, and timeout). --- * DcssbMatchManager:addPlayerByName(), DcssbMatchManager:addPlayerByUnit(): Methods to register players into a match, either by their player name or by the name of the unit they are controlling. These methods interact with dcsbot.sendBotTable() to communicate the player addition to the external bot. --- * DcssbMatchManager:onEvent(): Event handler function. It listens for DCS World events, specifically S_EVENT_PLAYER_ENTER_UNIT, to detect when players enter units. Based on the configured coalition and trigger zone/timeout settings, it adds players to the match. --- * DcssbMatchManager:onSchedule(): A scheduled function that runs repeatedly. It checks if players are waiting to enter a trigger zone and adds them to the match when they do. --- * DcssbMatchManager.addMatchManager(): A factory function to create and configure a new DcssbMatchManager instance. It also adds the instance as an event handler and schedules the onSchedule function. --- * DcssbMatchManager.addMatchManagersForZones(): Creates multiple DcssbMatchManager instances, one for each trigger zone defined in the mission. --- * DcssbMatchManager.knownEvents: A table mapping DCS event IDs and names to event objects. Used for event handling and debugging. --- * DcssbMatchManager.initialize(): Initializes the DcssbMatchManager.knownEvents table. --- --- The script is designed to be modular and extensible, allowing for easy creation and management of multiple matches within a single mission. --- The use of trigger zones and timeouts provides flexibility in how players are added to matches. --- --- Usage: --- To use this script, you would typically call DcssbMatchManager.addMatchManager() with the desired parameters (name, match name, coalition, trigger zone, and timeout). --- You can also call DcssbMatchManager.addMatchManagersForZones() to create match managers for all defined trigger zones in the mission. --- -- mocking DCSBot (for testing) --dcsbot = {} --function dcsbot.sendBotTable(table) -- env.info(string.format("DcssbMatchManager:dcsbot.sendBotTable->command=[%s]", table and table.command or "NONE")) -- env.info(string.format("DcssbMatchManager:dcsbot.sendBotTable->match_id=[%s]", table and table.match_id or "NONE")) -- env.info(string.format("DcssbMatchManager:dcsbot.sendBotTable->player_name=[%s]", table and table.player_name or "NONE")) --end DcssbMatchManager = {} DcssbMatchManager.Id = "DcssbMatchManager" DcssbMatchManager.Version = "1.0.4" DcssbMatchManager.LOG = true DcssbMatchManager.TRACE = false DcssbMatchManager.knownEvents = {} -- will be set at initialisation DcssbMatchManager.knownEventsNames = { [0] = "S_EVENT_INVALID", [1] = "S_EVENT_SHOT", [2] = "S_EVENT_HIT", [3] = "S_EVENT_TAKEOFF", [4] = "S_EVENT_LAND", [5] = "S_EVENT_CRASH", [6] = "S_EVENT_EJECTION", [7] = "S_EVENT_REFUELING", [8] = "S_EVENT_DEAD", [9] = "S_EVENT_PILOT_DEAD", [10] = "S_EVENT_BASE_CAPTURED", [11] = "S_EVENT_MISSION_START", [12] = "S_EVENT_MISSION_END", [13] = "S_EVENT_TOOK_CONTROL", [14] = "S_EVENT_REFUELING_STOP", [15] = "S_EVENT_BIRTH", [16] = "S_EVENT_HUMAN_FAILURE", [17] = "S_EVENT_DETAILED_FAILURE", [18] = "S_EVENT_ENGINE_STARTUP", [19] = "S_EVENT_ENGINE_SHUTDOWN", [20] = "S_EVENT_PLAYER_ENTER_UNIT", [21] = "S_EVENT_PLAYER_LEAVE_UNIT", [22] = "S_EVENT_PLAYER_COMMENT", [23] = "S_EVENT_SHOOTING_START", [24] = "S_EVENT_SHOOTING_END", [25] = "S_EVENT_MARK_ADDED", [26] = "S_EVENT_MARK_CHANGE", [27] = "S_EVENT_MARK_REMOVED", [28] = "S_EVENT_KILL", [29] = "S_EVENT_SCORE", [30] = "S_EVENT_UNIT_LOST", [31] = "S_EVENT_LANDING_AFTER_EJECTION", [32] = "S_EVENT_PARATROOPER_LENDING", [33] = "S_EVENT_DISCARD_CHAIR_AFTER_EJECTION", [34] = "S_EVENT_WEAPON_ADD", [35] = "S_EVENT_TRIGGER_ZONE", [36] = "S_EVENT_LANDING_QUALITY_MARK", [37] = "S_EVENT_BDA", [38] = "S_EVENT_AI_ABORT_MISSION", [39] = "S_EVENT_DAYNIGHT", [40] = "S_EVENT_FLIGHT_TIME", [41] = "S_EVENT_PLAYER_SELF_KILL_PILOT", [42] = "S_EVENT_PLAYER_CAPTURE_AIRFIELD", [43] = "S_EVENT_EMERGENCY_LANDING", [44] = "S_EVENT_UNIT_CREATE_TASK", [45] = "S_EVENT_UNIT_DELETE_TASK", [46] = "S_EVENT_SIMULATION_START", [47] = "S_EVENT_WEAPON_REARM", [48] = "S_EVENT_WEAPON_DROP", [49] = "S_EVENT_UNIT_TASK_COMPLETE", [50] = "S_EVENT_UNIT_TASK_STAGE", [51] = "S_EVENT_MAC_EXTRA_SCORE", [52] = "S_EVENT_MISSION_RESTART", [53] = "S_EVENT_MISSION_WINNER", [54] = "S_EVENT_RUNWAY_TAKEOFF", [55] = "S_EVENT_RUNWAY_TOUCH", [56] = "S_EVENT_MAC_LMS_RESTART", [57] = "S_EVENT_SIMULATION_FREEZE", [58] = "S_EVENT_SIMULATION_UNFREEZE", [59] = "S_EVENT_HUMAN_AIRCRAFT_REPAIR_START", [60] = "S_EVENT_HUMAN_AIRCRAFT_REPAIR_FINISH", [61] = "S_EVENT_MAX" } DcssbMatchManager.DEFAULT_TIMEOUT = 15 -- seconds DcssbMatchManager.matchManagers = {} -- list of match managers function DcssbMatchManager.init(object) -- technical name (identifier) object.name = nil -- match name object.matchName = nil -- coalition object.coalition = nil -- timeout in seconds object.timeout = DcssbMatchManager.DEFAULT_TIMEOUT -- trigger zone object.triggerZone = nil -- already managed player names object.playerNames = {} -- players waiting to pass in trigger zone object.unitsWaitingToPassInTriggerZoneToBeAdded = {} end function DcssbMatchManager:new(objectToCopy) if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager:new()")) end local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object DcssbMatchManager.init(objectToCreate) return objectToCreate end function DcssbMatchManager:setName(value) if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager[]:setName(%s)", value or "NONE")) end self.name = value return self end function DcssbMatchManager:getName() return self.name end function DcssbMatchManager:setMatchName(value) if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager[%s]:setMatchName(%s)", self:getName(), value or "NONE")) end self.matchName = value return self end function DcssbMatchManager:getMatchName() return self.matchName end function DcssbMatchManager:setCoalition(value) if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager[%s]:setCoalition(%s)", self:getName(), value or "NONE")) end self.coalition = value return self end function DcssbMatchManager:getCoalition() return self.coalition end function DcssbMatchManager:setTriggerZone(value) if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager[%s]:setTriggerZone(%s)", self:getName(), value or "NONE")) end if value then if type(value) == "string" then value = trigger.misc.getZone(value) if value then if DcssbMatchManager.LOG then env.info("Zone found") end else env.error(string.format("DcssbMatchManager[%s]:setTriggerZone(%s) - zone not found", self:getName(), value)) self.triggerZone = nil return self end end self.triggerZone = value end return self end function DcssbMatchManager:getTriggerZone() return self.triggerZone end function DcssbMatchManager:setTimeout(value) if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager[%s]:setTimeout(%s)", self:getName(), value or "NONE")) end self.timeout = value return self end function DcssbMatchManager:getTimeout() return self.timeout end function DcssbMatchManager.addPlayerByNameForScheduler(parameters) if parameters then local self = parameters[1] local playerName = parameters[2] self:addPlayerByName(playerName) end end function DcssbMatchManager:addPlayerByName(playerName) if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager[%s]:addPlayerByName(%s)", self:getName(), playerName or "NONE")) end if playerName then if dcsbot then dcsbot.sendBotTable({ command = "addPlayerToMatch", match_id = self:getMatchName(), player_name = playerName }) end trigger.action.outText("Player " .. playerName .. " added to match " .. self:getMatchName(), 10) if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager[%s]: Player %s added to match %s", self:getName(), playerName, self:getMatchName())) end end self.playerNames[playerName] = playerName return self end function DcssbMatchManager.addPlayerByUnitForScheduler(parameters) if parameters then local self = parameters[1] local unitName = parameters[2] self:addPlayerByUnit(unitName) end end function DcssbMatchManager:addPlayerByUnit(unitName) if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager[%s]:addPlayerByUnit(%s)", self:getName(), unitName or "NONE")) end if unitName then local unit = Unit.getByName(unitName) if unit then local playerName = unit:getPlayerName() if playerName then self:addPlayerByName(playerName) end end end end function DcssbMatchManager:addUnitWaitingToPassInTriggerZoneToBeAdded(unitName) if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager[%s]:addUnitWaitingToPassInTriggerZoneToBeAdded(%s)", self:getName(), unitName or "NONE")) end if unitName then self.unitsWaitingToPassInTriggerZoneToBeAdded[unitName] = unitName end end function DcssbMatchManager:onSchedule() if DcssbMatchManager.TRACE then env.info(string.format("DcssbMatchManager[%s]:onSchedule()", self:getName())) end -- reschedule timer.scheduleFunction(DcssbMatchManager.onSchedule, self, timer.getTime() + 1) -- schedule in 1 second -- check if players are waiting to pass in trigger zone if self:getTriggerZone() then for unitName, _ in pairs(self.unitsWaitingToPassInTriggerZoneToBeAdded) do if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager[%s]:onSchedule() - player in unit %s is waiting to pass in trigger zone", self:getName(), unitName)) end local unit = Unit.getByName(unitName) if not unit then if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager[%s]:onSchedule() - player in unit %s is not in game anymore", self:getName(), unitName)) end self.unitsWaitingToPassInTriggerZoneToBeAdded[unitName] = nil else local unitPoint = unit:getPoint() local zonePoint = self:getTriggerZone() and self:getTriggerZone().point local zoneRadius = self:getTriggerZone() and self:getTriggerZone().radius if not zonePoint or not zoneRadius then env.error(string.format("DcssbMatchManager[%s]:onSchedule() - trigger zone is not correctly defined", self:getName())) return end if ((unitPoint.x - zonePoint.x)^2 + (unitPoint.z - zonePoint.z)^2)^0.5 <= zoneRadius then if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager[%s]:onSchedule() - player in unit %s is passing in trigger zone", self:getName(), unitName)) end -- if there is a timeout, schedule the player to be added to the match if self:getTimeout() then if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager[%s]:onSchedule() - player in unit %s will be added to the match in %s seconds", self:getName(), unitName, self:getTimeout())) end timer.scheduleFunction(DcssbMatchManager.addPlayerByUnitForScheduler, {self, unitName}, timer.getTime() + self:getTimeout()) else if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager[%s]:onSchedule() - player in unit %s is being added to the match", self:getName(), unitName)) end self:addPlayerByUnit(unitName) end self.unitsWaitingToPassInTriggerZoneToBeAdded[unitName] = nil end end end end end function DcssbMatchManager.addMatchManager(name, matchName, coalition, triggerZone, timeout) if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager.addMatchManager(%s)", name or "NONE")) end local matchManager = DcssbMatchManager:new() matchManager:setName(name) matchManager:setMatchName(matchName) matchManager:setCoalition(coalition) matchManager:setTriggerZone(triggerZone) matchManager:getTriggerZone() matchManager:setTimeout(timeout) -- add the match manager instance to the list of match managers table.insert(DcssbMatchManager.matchManagers, matchManager) -- schedule the match manager matchManager:onSchedule() return matchManager end function DcssbMatchManager.addMatchManagersForZones(timeout, exclusionList) if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager.addMatchManagersForZones(%s)", timeout or "NONE")) end local matchManagers = {} for _, zone in pairs(env.mission.triggers.zones) do local name = zone.name --veaf.loggers.get(DcssbMatchManager.Id):trace("name=%s)", name) --veaf.loggers.get(DcssbMatchManager.Id):trace("zone=%s)", zone) if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager.addMatchManagersForZones() - testing zone %s", name or "NONE")) end local exclude = false if exclusionList then for _, value in ipairs(exclusionList) do if value:upper() == name:upper() then exclude = true end end end if not exclude then if zone then if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager.addMatchManagersForZones() - adding zone %s", name or "NONE")) end local matchManager = DcssbMatchManager.addMatchManager(name, name, nil, name, timeout) table.insert(matchManagers, matchManager) end else if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager.addMatchManagersForZones() - zone %s is excluded", name)) end end end return matchManagers end DcssbMatchManager.eventHandler = {} function DcssbMatchManager.eventHandler:onEvent(event) local function completeUnitFromName(unitName) if DcssbMatchManager.TRACE then env.info(string.format("DcssbMatchManager.completeUnitFromName(unitName=%s)", unitName or "NONE")) end if unitName ~= nil then local unitType = nil local unitLifePercent = nil local unit = Unit.getByName(unitName) if unit and unit.getTypeName then unitType = unit:getTypeName() local unitLife = unit:getLife() local unitLife0 = 0 if unit.getLife0 then -- statics have no life0 unitLife0 = unit:getLife0() end unitLifePercent = unitLife if unitLife0 > 0 then unitLifePercent = 100 * unitLife / unitLife0 end end return { unitName = unitName, unitType = unitType, unitPilotName = (unit and unit:getPlayerName() or "unknown"), unitLifePercent = unitLifePercent } else return nil end end local function completeUnit(unit) if unit ~= nil and unit.getName then local unitName = unit:getName() return completeUnitFromName(unitName) else return nil end end local function transformEvent(event) local _event = { type = DcssbMatchManager.knownEvents[event.id], time = event.time, idx = event.idx, coordinates = event.pos, text = event.text, coalition = event.coalition, groupId = event.groupID, place = completeUnit(event.place), birthPlace = event.subPlace, initiator = completeUnit(event.initiator), target = completeUnit(event.target), weapon = event.weapon, weaponName = event.weapon_name, comment = event.comment } return _event end if event == nil then env.error("DcssbMatchManager.onEvent was called with a nil event!") return end local _event = transformEvent(event) -- process birth event local eventId = (event and event.id or 0) if DcssbMatchManager.TRACE then env.info(string.format("DcssbMatchManager.onEvent() - eventId=%s", eventId or "NONE")) end if eventId == world.event.S_EVENT_PLAYER_ENTER_UNIT then -- browse all the known match managers for _, matchManager in pairs(DcssbMatchManager.matchManagers) do -- check the coalition of the event initiator if matchManager:getCoalition() then if _event.initiator and _event.initiator.unitName then local unit = Unit.getByName(_event.initiator.unitName) if unit then if unit:getCoalition() ~= matchManager:getCoalition() then return end end end end -- if there is a trigger zone, don't add player to the match now if matchManager:getTriggerZone() then if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager.onEvent() - player %s is waiting to pass in trigger zone", _event.initiator.unitPilotName)) end matchManager:addUnitWaitingToPassInTriggerZoneToBeAdded(_event.initiator.unitName) return end -- if there is a timeout, schedule the player to be added to the match if matchManager:getTimeout() then if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager.onEvent() - player %s will be added to the match in %s seconds", _event.initiator.unitPilotName, matchManager:getTimeout())) end timer.scheduleFunction(DcssbMatchManager.addPlayerByNameForScheduler, {matchManager, _event.initiator.unitPilotName}, timer.getTime() + matchManager:getTimeout()) else if DcssbMatchManager.LOG then env.info(string.format("DcssbMatchManager.onEvent() - player %s is being added to the match", _event.initiator.unitPilotName)) end matchManager:addPlayerByName(_event.initiator.unitPilotName) end end end end function DcssbMatchManager.initialize() if DcssbMatchManager.LOG then env.info("DcssbMatchManager.initialize() version "..DcssbMatchManager.Version) end -- prepare the events maps for eventId, eventName in pairs(DcssbMatchManager.knownEventsNames) do local event = { name = eventName, id = eventId, enabled = true --false } DcssbMatchManager.knownEvents[eventName] = event DcssbMatchManager.knownEvents[eventId] = event end -- initialize the event handler -- Add event handler. world.addEventHandler(DcssbMatchManager.eventHandler) end DcssbMatchManager.initialize() --veaf.loggers.new(DcssbMatchManager.Id, "trace") env.info("DcssbMatchManager - script loaded")