------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VEAF server hook for DCS World -- By Zip (2021) -- -- Features: -- --------- -- * This hook is used on the VEAF server. It has multiple features : -- * - autorestart at night when no one is connected -- * - listen to chat text and extract commands to be run on the server (WIP) -- * - open a socket to listen to specific commands and push them on the server (TBD) -- -- Usage: -- --------- -- * - Drop this script in the Scripts/Hooks folder of the server ("saved games" !) -- * - Also drop the `veaf-pilots.lua` file in the main server folder ("saved games" !) and edit it ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafServerHook = {} base = _G require = base.require io = require('io') lfs = require('lfs') os = require('os') package.path = package.path..";"..lfs.currentdir().."/LuaSocket/?.lua"..";"..lfs.writedir() .. "/Mods/services/BufferingSocket/lua/?.lua" package.cpath = package.cpath..";"..lfs.currentdir().."/LuaSocket/?.dll"..';'.. lfs.writedir()..'/Mods/services/BufferingSocket/bin/' ..'?.dll;' veafServerHook.config = require "BufferingSocketConfig" BufferingSocket = require('BufferingSocket') DCS_DIR = lfs.writedir() VEAF_SERVER_DIR = DCS_DIR .. [[scripts\hooks\]] VEAF_PILOTS_FILE = "veaf-pilots.txt" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in the log will start with this. veafServerHook.Id = "VEAFHOOK - " --- Version. veafServerHook.Version = "2.5.0" -- trace level, specific to this module veafServerHook.Trace = false veafServerHook.Debug = false veafServerHook.CommandStarter = "/" veafServerHook.CommandParser = "/([a-zA-Z0-9]+)%s?(.*)" veafServerHook.ADMIN_FAKE_UCID = "0123456789ABCDEF012345-AdminVEAF" -- frequency of checks for server restart, outside of pilot disconnecting veafServerHook.FRAME_CHECK_FREQUENCY_IN_SECONDS = 30 -- maximum mission duration before the server is restarted (in minutes, mission model time) veafServerHook.DEFAULT_MAX_MISSION_DURATION = 2 * 60 -- maximum server uptime before it is restarted (in minutes) veafServerHook.DEFAULT_MAX_SERVER_UPTIME = 2 * 60 -- maximum number of players before allowing restart of the server veafServerHook.DEFAULT_MAX_PLAYERS_FOR_RESTART = 1 -- scripts injected in the mission REGISTER_PLAYER = [[ if veafRemote and veafRemote.registerUser then veafRemote.registerUser("%s", "%s", "%s") end ]] REGISTER_PLAYER_SLOT = [[ if veafRemote and veafRemote.registerUserSlot then veafRemote.registerUserSlot("%s", "%s", "%s") end ]] RUN_COMMAND = [[ if veafRemote and veafRemote.executeCommandFromRemote then veafRemote.executeCommandFromRemote("%s", "%s", "%s", "%s", "%s") end ]] SEND_MESSAGE = [[ if trigger and trigger.action and trigger.action.outText then trigger.action.outText("%s", %s) end ]] -- marks the end of a data package sent to the API server socket veafServerHook.EOT_MARKER = ">>EOT" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafServerHook.pilots = {} veafServerHook.closeServerAtMissionStop = false veafServerHook.closeServerAtLastDisconnect = true veafServerHook.lastUDPFrameTime = 0 veafServerHook.lastServerUptimeCheckFrameTime = 0 veafServerHook.maxMissionDuration = veafServerHook.DEFAULT_MAX_MISSION_DURATION veafServerHook.maxServerUptime = veafServerHook.DEFAULT_MAX_SERVER_UPTIME veafServerHook.maxPlayersForRestart = veafServerHook.DEFAULT_MAX_PLAYERS_FOR_RESTART veafServerHook.statisticsTypes = {"ping", "crashes", "vehicules", "aircrafts", "ships", "score", "landings", "ejections"} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafServerHook.logError(message) log.write(veafServerHook.Id, log.ERROR, message) end function veafServerHook.logWarning(message) log.write(veafServerHook.Id, log.WARNING, message) end function veafServerHook.logInfo(message) log.write(veafServerHook.Id, log.INFO, message) end function veafServerHook.logDebug(message) if message and veafServerHook.Debug then log.write(veafServerHook.Id, log.INFO, message) end end function veafServerHook.logTrace(message) if message and veafServerHook.Trace then log.write(veafServerHook.Id, log.INFO, message) end end function veafServerHook.p(o, level) local MAX_LEVEL = 20 if level == nil then level = 0 end if level > MAX_LEVEL then veafServerHook.logError("max depth reached in p : "..tostring(MAX_LEVEL)) return "" end local text = "" if (type(o) == "table") then text = "\n" for key,value in pairs(o) do for i=0, level do text = text .. " " end text = text .. ".".. key.."="..veafServerHook.p(value, level+1) .. "\n"; end elseif (type(o) == "function") then text = "[function]"; elseif (type(o) == "boolean") then if o == true then text = "[true]"; else text = "[false]"; end else if o == nil then text = "[nil]"; else text = tostring(o); end end return text end -------------------------------------------------------------------------------------------------------------------------------------- -- DCS events handling ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafServerHook.onSimulationStart() veafServerHook.logDebug(string.format("veafServerHook.onSimulationStart()")) veafServerHook.initialize() -- ask the mission for its maximum runtime veafServerHook.logDebug(string.format("ask the mission for its maximum runtime")) local _maxDuration = nil local _status, _retValue = pcall(net.dostring_in, 'mission', 'return a_do_script(' .. '[===[ if veaf and veaf.getMissionMaxRuntime then return veaf.getMissionMaxRuntime() else return nil end ]===]' .. ')') veafServerHook.logTrace(string.format("_status=%s",veafServerHook.p(_status))) veafServerHook.logTrace(string.format("_retValue=%s",veafServerHook.p(_retValue))) if not _status then veafServerHook.logWarning(string.format("Code injection failed for veaf.getMissionMaxRuntime()")) else if type(_retValue) == "string" and _retValue:match("%d+") then _maxDuration = tonumber(_retValue) end end veafServerHook.maxMissionDuration = _maxDuration if veafServerHook.maxMissionDuration == nil then veafServerHook.maxMissionDuration = veafServerHook.DEFAULT_MAX_MISSION_DURATION veafServerHook.logInfo(string.format("Maximum mission duration is set to its default value (%s)", veafServerHook.p(veafServerHook.maxMissionDuration))) else veafServerHook.logInfo(string.format("Maximum mission duration is set to %s", veafServerHook.p(veafServerHook.maxMissionDuration))) end veafServerHook.maxServerUptime = veafServerHook.DEFAULT_MAX_SERVER_UPTIME veafServerHook.maxPlayersForRestart = veafServerHook.DEFAULT_MAX_PLAYERS_FOR_RESTART veafServerHook.logInfo(string.format("Maximum number of players for restart is set to its default value (%s)", veafServerHook.p(veafServerHook.maxPlayersForRestart))) veafServerHook.closeServerAtLastDisconnect = true -- halt the server when the mission stops, by default end function veafServerHook.onSimulationStop() veafServerHook.logDebug(string.format("veafServerHook.onSimulationStop()")) if veafServerHook.closeServerAtMissionStop then veafServerHook.logInfo(string.format("veafServerHook.onSimulationStop() - stopping the server")) Sim.exitProcess() end end function veafServerHook.onPlayerConnect(id) veafServerHook.logDebug(string.format("veafServerHook.onPlayerConnect([%s])", veafServerHook.p(id))) local _playerDetails = net.get_player_info( id ) local playerName = _playerDetails.name local ucid = _playerDetails.ucid veafServerHook.logTrace(string.format("playerName=%s",veafServerHook.p(playerName))) veafServerHook.logTrace(string.format("ucid=%s",veafServerHook.p(ucid))) -- parse the message local pilot = veafServerHook.pilots[ucid] if not pilot then veafServerHook.logInfo(string.format("Unknown pilot [%s] connecting with UCID [%s]", veafServerHook.p(playerName), veafServerHook.p(ucid))) pilot = { level = -1} -- no power at all else veafServerHook.logInfo(string.format("VEAF pilot [%s] connecting with UCID [%s]", veafServerHook.p(playerName), veafServerHook.p(ucid))) end veafServerHook.logTrace(string.format("pilot=%s",veafServerHook.p(pilot))) local payload = string.format(REGISTER_PLAYER, playerName, pilot.level, ucid) veafServerHook.logTrace(string.format("payload=%s",veafServerHook.p(payload))) veafServerHook.injectCode(payload) end function veafServerHook.onPlayerChangeSlot(id) veafServerHook.logDebug(string.format("veafServerHook.onPlayerChangeSlot([%s])", veafServerHook.p(id))) local _playerDetails = net.get_player_info(id) if not _playerDetails then veafServerHook.logError(string.format("veafServerHook.onPlayerChangeSlot([%s]) - _playerDetails is nil", veafServerHook.p(id))) return end local playerName = _playerDetails.name local ucid = _playerDetails.ucid local unitName = nil veafServerHook.logTrace(string.format("_playerDetails.slot=%s",veafServerHook.p(_playerDetails.slot))) if _playerDetails.side ~= 0 and _playerDetails.slot ~= "" and _playerDetails.slot ~= nil then local slot = _playerDetails.slot if string.find(tostring(slot), "_", 1, true) then --extract substring - get the seat ID slot = string.sub(slot, 1, string.find(slot, "_", 1, true)-1) end veafServerHook.logTrace(string.format("slot=%s",veafServerHook.p(slot))) unitName = Sim.getUnitProperty(slot, Sim.UNIT_NAME) end veafServerHook.logTrace(string.format("playerName=%s",veafServerHook.p(playerName))) veafServerHook.logTrace(string.format("ucid=%s",veafServerHook.p(ucid))) veafServerHook.logTrace(string.format("unitName=%s",veafServerHook.p(unitName))) -- set the player current unit name local payload = string.format(REGISTER_PLAYER_SLOT, playerName, ucid, unitName or "nil") -- unitName will be nil if the player is a spectator veafServerHook.logTrace(string.format("payload=%s",veafServerHook.p(payload))) veafServerHook.injectCode(payload) end function veafServerHook.onPlayerDisconnect(id, err_code) veafServerHook.logDebug(string.format("veafServerHook.onPlayerDisconnect([%s], [%s])", veafServerHook.p(id), veafServerHook.p(err_code))) veafServerHook.stopMissionIfNeeded() end function veafServerHook.onChatMessage(message, from) veafServerHook.logDebug(string.format("veafServerHook.onChatMessage([%s], [%s])",veafServerHook.p(from), veafServerHook.p(message))) -- try and recognize a command if message ~= nil and message:lower():sub(1, #veafServerHook.CommandStarter) == veafServerHook.CommandStarter then local _playerDetails = net.get_player_info( from ) if _playerDetails ~=nil then local playerName = _playerDetails.name local ucid = _playerDetails.ucid local unitName = nil veafServerHook.logTrace(string.format("_playerDetails.slot=%s",veafServerHook.p(_playerDetails.slot))) if _playerDetails.side ~= 0 and _playerDetails.slot ~= "" and _playerDetails.slot ~= nil then local slot = _playerDetails.slot if string.find(tostring(slot), "_", 1, true) then --extract substring - get the seat ID slot = string.sub(slot, 1, string.find(slot, "_", 1, true)-1) end veafServerHook.logTrace(string.format("slot=%s",veafServerHook.p(slot))) unitName = Sim.getUnitProperty(slot, Sim.UNIT_NAME) end veafServerHook.logTrace(string.format("playerName=%s",veafServerHook.p(playerName))) veafServerHook.logTrace(string.format("ucid=%s",veafServerHook.p(ucid))) veafServerHook.logTrace(string.format("unitName=%s",veafServerHook.p(unitName))) -- parse the message local pilot = veafServerHook.pilots[ucid] if from == 1 then -- this is the server administrator pilot = veafServerHook.pilots[veafServerHook.ADMIN_FAKE_UCID] end veafServerHook.logTrace(string.format("pilot=%s",veafServerHook.p(pilot))) if veafServerHook.parse(pilot, playerName, ucid, unitName, message) then veafServerHook.logInfo(string.format("Player %s ran command %s", playerName, message)) else veafServerHook.logWarning(string.format("Player %s was denied running command %s", playerName, message)) end end end return false end function veafServerHook.onSimulationFrame() --veafServerHook.logTrace(string.format("veafServerHook.onSimulationFrame()")) local _now = Sim.getRealTime() if _now > veafServerHook.lastServerUptimeCheckFrameTime + veafServerHook.FRAME_CHECK_FREQUENCY_IN_SECONDS then veafServerHook.lastServerUptimeCheckFrameTime = _now veafServerHook.logTrace(string.format("checking server uptime")) veafServerHook.stopMissionIfNeeded() end if veafServerHook.config.activate and _now > veafServerHook.lastUDPFrameTime + veafServerHook.config.refreshDelay then veafServerHook.lastUDPFrameTime = _now veafServerHook.logTrace(string.format("sending data")) veafServerHook.sendData(_now) end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Core methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafServerHook.sendData(timestamp) local _now = Sim.getRealTime() local data_package = { timestamp = _now} veafServerHook.logTrace(string.format("get basic server information")) data_package.serverData = { frameTime = _now, mission = Sim.getMissionFilename(), missionTimeInSeconds = Sim.getModelTime(), missionMaxTimeInSeconds = veafServerHook.maxMissionDuration * 60, serverMaxUptimeInSeconds = veafServerHook.maxServerUptime * 60, numberOfPlayers = #net.get_player_list() - 1, maxPlayersForRestart = veafServerHook.maxPlayersForRestart } -- get list of connected pilots data_package.pilots = {} veafServerHook.logTrace(string.format("get list of connected pilots")) local players = net.get_player_list() for playerId, _ in pairs(players) do veafServerHook.logTrace(string.format("playerId=%s",playerId)) local playerDetails = net.get_player_info(playerId) local playerName = playerDetails.name local ucid = playerDetails.ucid local pilotData = { name = playerName, level = 0, unit = playerDetails.slot, stats = {} } for key, value in pairs(veafServerHook.statisticsTypes) do local stat = net.get_stat(playerId, key) veafServerHook.logTrace(string.format("stat[%s]=%s",veafServerHook.p(key),veafServerHook.p(stat))) pilotData.stats[value] = stat end local pilot = veafServerHook.pilots[ucid] if pilot then pilotData.level = pilot.level end data_package.pilots[ucid] = pilotData end -- prepare the data package veafServerHook.logTrace(string.format("prepare the data package")) veafServerHook.logTrace(string.format("data_package=%s",veafServerHook.p(data_package))) local _payload = net.lua2json(data_package); veafServerHook.logTrace(string.format("_payload=%s",veafServerHook.p(_payload))) -- send the payload veafServerHook.logTrace(string.format("send the payload")) BufferingSocket.send(_payload) veafServerHook.logTrace(string.format("send the EOT")) BufferingSocket.send(veafServerHook.EOT_MARKER) end function veafServerHook.parse(pilot, playerName, ucid, unitName, message) veafServerHook.logTrace(string.format("veafServerHook.parse([%s] , [%s])", veafServerHook.p(playerName), veafServerHook.p(message))) veafServerHook.logTrace(string.format("pilot=%s",veafServerHook.p(pilot))) veafServerHook.logTrace(string.format("unitName=%s",veafServerHook.p(unitName))) if not pilot then veafServerHook.logWarning(string.format("Unknown pilot [%s] sent chat message [%s])",veafServerHook.p(playerName), veafServerHook.p(message))) end local _module, _command = message:match(veafServerHook.CommandParser) veafServerHook.logTrace(string.format("_module=%s",veafServerHook.p(_module))) veafServerHook.logTrace(string.format("_command=%s",veafServerHook.p(_command))) if pilot.level > 0 then -- register the player local payload = string.format(REGISTER_PLAYER, playerName, pilot.level, ucid) veafServerHook.logTrace(string.format("payload=%s",veafServerHook.p(payload))) veafServerHook.injectCode(payload) end if _module and _module:lower() == "send" then -- any registered pilot can call the TEST commands if pilot.level >= 0 then local _command = _command or "this is a test message from the VEAF server hook" veafServerHook.logInfo(string.format("[%s] is sending message [%s])",veafServerHook.p(playerName), veafServerHook.p(_command))) veafServerHook.sendMessage(_command, 10) return true end elseif _module and _module:lower() == "code" then -- only level >= 90 can execute code if pilot.level >= 90 then return veafServerHook.injectCode(_command) end elseif _module and _module:lower() == "restart" then -- only level >= 10 can schedule mission restart if pilot.level >= 10 then veafServerHook.maxMissionDuration = 0 veafServerHook.maxServerUptime = 0 veafServerHook.closeServerAtLastDisconnect = false local _message = string.format("[%s] is asking for mission restart when the last pilot disconnects from the server",veafServerHook.p(playerName)) veafServerHook.logInfo(_message) veafServerHook.sendMessage(_message, 10) veafServerHook.stopMissionIfNeeded() return true end elseif _module and _module:lower() == "restartnow" then -- only level >= 30 can schedule mission restart without waiting for all to disconnect if pilot.level >= 30 then veafServerHook.maxMissionDuration = 0 veafServerHook.maxServerUptime = 0 veafServerHook.maxPlayersForRestart = 666 veafServerHook.closeServerAtLastDisconnect = false local _message = string.format("[%s] is asking for mission restart now",veafServerHook.p(playerName)) veafServerHook.logInfo(_message) veafServerHook.sendMessage(_message, 10) veafServerHook.stopMissionIfNeeded() return true end elseif _module and _module:lower() == "halt" then -- only level >= 10 can schedule server halt (and hopefully autorestart) if pilot.level >= 10 then veafServerHook.maxMissionDuration = 0 veafServerHook.maxServerUptime = 0 veafServerHook.closeServerAtLastDisconnect = true local _message = string.format("[%s] is asking for server halt when the last pilot disconnects from the server",veafServerHook.p(playerName)) veafServerHook.logInfo(_message) veafServerHook.sendMessage(_message, 10) veafServerHook.stopMissionIfNeeded() return true end elseif _module and _module:lower() == "haltnow" then -- only level >= 50 can trigger server halt without waiting for all to disconnect if pilot.level >= 50 then veafServerHook.maxMissionDuration = 0 veafServerHook.maxServerUptime = 0 veafServerHook.closeServerAtMissionStop = true veafServerHook.maxPlayersForRestart = 666 veafServerHook.onSimulationStop() return true end elseif _module and _module:lower() == "pause" then -- only level >= 10 can pause and unpause the server if pilot.level >= 10 then local pause = Sim.getPause() local onoff = "on" if pause then onoff = "off" end local _message = string.format("[%s] is setting the server %s pause",veafServerHook.p(playerName), onoff) veafServerHook.logInfo(_message) veafServerHook.sendMessage(_message, 10) Sim.setPause(not pause) return true end else -- only level >= 1 can call commands if pilot.level >= 1 then local payload = string.format(RUN_COMMAND, tostring(playerName), tostring(pilot.level), tostring(unitName), tostring(_module), tostring(_command)) veafServerHook.logTrace(string.format("payload=%s",veafServerHook.p(payload))) return veafServerHook.injectCode(payload) end end return false end function veafServerHook.stopMissionIfNeeded() veafServerHook.logTrace(string.format("veafServerHook.stopMissionIfNeeded()")) local _modelTimeInSeconds = Sim.getModelTime() veafServerHook.logTrace(string.format("_modelTimeInSeconds=%s",veafServerHook.p(_modelTimeInSeconds))) local _realTimeInSeconds = Sim.getRealTime() veafServerHook.logTrace(string.format("_realTimeInSeconds=%s",veafServerHook.p(_realTimeInSeconds))) if (_modelTimeInSeconds >= veafServerHook.maxMissionDuration * 60) or (_realTimeInSeconds >= veafServerHook.maxServerUptime * 60) then -- check if no one is connected (triggered on last disconnect) local _players = net.get_player_list() veafServerHook.logTrace(string.format("_players=%s",veafServerHook.p(_players))) local _nPlayers = #_players veafServerHook.logTrace(string.format("_nPlayers=%s",veafServerHook.p(_nPlayers))) if _nPlayers <= veafServerHook.maxPlayersForRestart then -- only the administrator remains -- restart the server veafServerHook.logInfo(string.format("veafServerHook.stopMissionIfNeeded() - stopping the mission")) if veafServerHook.closeServerAtLastDisconnect then veafServerHook.closeServerAtMissionStop = true Sim.stopMission() else -- just restart the mission veafServerHook.closeServerAtMissionStop = false local _missionFilename = Sim.getMissionFilename() veafServerHook.logInfo(string.format("reloading mission [%s]", _missionFilename)) net.load_mission(_missionFilename) end end else veafServerHook.logInfo(string.format("veafServerHook.stopMissionIfNeeded() - no need to stop the mission")) end end function veafServerHook.injectCode(payload) veafServerHook.logDebug(string.format("veafServerHook.injectCode([%s])",veafServerHook.p(payload))) local _status, _retValue = pcall(net.dostring_in, 'mission', 'return a_do_script(' .. '[===[' .. payload .. ']===]' .. ')') veafServerHook.logTrace(string.format("_status=%s",veafServerHook.p(_status))) veafServerHook.logTrace(string.format("_retValue=%s",veafServerHook.p(_retValue))) if not _status then veafServerHook.logError(string.format("Code injection failed for [%s]",veafServerHook.p(payload))) veafServerHook.logError(string.format("_retValue=[%s], _status=[%s]",veafServerHook.p(_retValue), veafServerHook.p(_status))) end return _retValue end function veafServerHook.sendMessage(message, duration) veafServerHook.logDebug(string.format("veafServerHook.sendMessage([%s, %s])",veafServerHook.p(message), veafServerHook.p(duration))) veafServerHook.injectCode(string.format(SEND_MESSAGE, message, tostring(duration))) end -- Load the list of VEAF pilots function veafServerHook.loadPilots() veafServerHook.logDebug(string.format("veafServerHook.loadPilots()")) veafServerHook.logInfo(string.format("loading pilots")) local filepath = VEAF_SERVER_DIR .. VEAF_PILOTS_FILE local file = assert(loadfile(filepath)) if not file then veafServerHook.logError(string.format("Error while loading pilots list file [%s]",veafServerHook.p(filepath))) return end file() local returner = loadstring("return pilots") if returner then veafServerHook.pilots = returner() veafServerHook.logInfo(string.format("pilots loaded")) veafServerHook.logTrace(string.format("pilots=%s",veafServerHook.p(veafServerHook.pilots))) else veafServerHook.logError(string.format("error while loading pilots")) end end function veafServerHook.initialize() veafServerHook.logDebug(string.format("veafServerHook.initialize")) veafServerHook.logInfo(string.format("initializing module")) veafServerHook.loadPilots() end -- set up the socket to call the web server if veafServerHook.config.activate then veafServerHook.logDebug(string.format("set up the socket to call the web server; host=%s and port=%s", veafServerHook.p(veafServerHook.config.host), veafServerHook.p(veafServerHook.config.port))) BufferingSocket.startSession(veafServerHook.config.host, veafServerHook.config.port) end veafServerHook.logDebug(string.format("registering DCS callbacks")) Sim.setUserCallbacks(veafServerHook)