diff --git a/plugins/adminpanel.smx b/plugins/adminpanel.smx index 037e272..4367f50 100644 Binary files a/plugins/adminpanel.smx and b/plugins/adminpanel.smx differ diff --git a/scripting/adminpanel.sp b/scripting/adminpanel.sp index acfb590..e862d11 100644 --- a/scripting/adminpanel.sp +++ b/scripting/adminpanel.sp @@ -2,12 +2,10 @@ #define DEBUG -// Update intervals (only sends when > 0 players) -// The update interval when there are active viewers -#define UPDATE_INTERVAL 5.0 -// The update interval when there are no viewers on. -// We still need to poll to know how many viewers are watching -#define UPDATE_INTERVAL_SLOW 20.0 +// Every attempt waits exponentionally longer, up to this value. +#define MAX_ATTEMPT_TIMEOUT 120.0 +#define DEFAULT_SERVER_PORT 7888 +#define SOCKET_TIMEOUT_DURATION 90.0 #include #include @@ -15,6 +13,7 @@ #include #include #include +#include #pragma newdecls required @@ -27,102 +26,277 @@ public Plugin myinfo = url = "https://github.com/jackzmc/l4d2-admin-dash" }; +int LIVESTATUS_VERSION = 0; + + ConVar cvar_debug; -ConVar cvar_postAddress; char postAddress[128]; -ConVar cvar_authKey; char authKey[512]; ConVar cvar_gamemode; char gamemode[32]; +ConVar cvar_difficulty; int gameDifficulty; +ConVar cvar_id; char serverId[32]; +ConVar cvar_address; char serverIp[16] = "127.0.0.1"; int serverPort = DEFAULT_SERVER_PORT; char currentMap[64]; int numberOfPlayers = 0; -int lastSuccessTime; int campaignStartTime; -int lastErrorCode; int uptime; -bool fastUpdateMode = false; - -Handle updateTimer = null; +bool g_inTransition; +bool isL4D1Survivors; +int lastReceiveTime; char steamidCache[MAXPLAYERS+1][32]; char nameCache[MAXPLAYERS+1][MAX_NAME_LENGTH]; int g_icBeingHealed[MAXPLAYERS+1]; int playerJoinTime[MAXPLAYERS+1]; +Handle updateHealthTimer[MAXPLAYERS+1]; +Handle updateItemTimer[MAXPLAYERS+1]; +Handle receiveTimeoutTimer = null; +bool lateLoaded; + +Socket g_socket; +bool g_isPaused; +#define BUFFER_SIZE 2048 +Buffer sendBuffer; +Buffer receiveBuffer; // Unfortunately there's no easy way to have this not be the same as BUFFER_SIZE + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + lateLoaded = late; + return APLRes_Success; +} public void OnPluginStart() { + // TODO: periodic reconnect + g_socket = new Socket(SOCKET_TCP, OnSocketError); + g_socket.SetOption(SocketKeepAlive, 1); + g_socket.SetOption(SocketReuseAddr, 1); + uptime = GetTime(); cvar_debug = CreateConVar("sm_adminpanel_debug", "0", "Turn on debug mode", FCVAR_DONTRECORD, true, 0.0, true, 1.0); - cvar_postAddress = CreateConVar("sm_adminpanel_url", "", "The base address to post updates to", FCVAR_NONE); - cvar_postAddress.AddChangeHook(OnCvarChanged); - cvar_postAddress.GetString(postAddress, sizeof(postAddress)); - cvar_authKey = CreateConVar("sm_adminpanel_key", "", "The authentication key", FCVAR_NONE); - cvar_authKey.AddChangeHook(OnCvarChanged); - cvar_authKey.GetString(authKey, sizeof(authKey)); + cvar_id = CreateConVar("sm_adminpanel_id", "", "The server ID to post updates for", FCVAR_NONE); + cvar_id.AddChangeHook(OnCvarChanged); + cvar_id.GetString(serverId, sizeof(serverId)); + + cvar_address = CreateConVar("sm_adminpanel_host", "100.108.152.125:7888", "The IP and port to connect to, default is 7888", FCVAR_NONE); + cvar_address.AddChangeHook(OnCvarChanged); + cvar_address.GetString(serverIp, sizeof(serverIp)); + OnCvarChanged(cvar_address, "", serverIp); cvar_gamemode = FindConVar("mp_gamemode"); cvar_gamemode.AddChangeHook(OnCvarChanged); cvar_gamemode.GetString(gamemode, sizeof(gamemode)); + cvar_difficulty = FindConVar("z_difficulty"); + cvar_difficulty.AddChangeHook(OnCvarChanged); + gameDifficulty = GetDifficultyInt(); + HookEvent("game_init", Event_GameStart); HookEvent("game_end", Event_GameEnd); - HookEvent("heal_success", Event_HealStop); - HookEvent("heal_interrupted", Event_HealStop); + HookEvent("heal_begin", Event_HealStart); + HookEvent("heal_success", Event_HealSuccess); + HookEvent("heal_interrupted", Event_HealInterrupted); + HookEvent("pills_used", Event_ItemUsed); + HookEvent("adrenaline_used", Event_ItemUsed); + HookEvent("weapon_drop", Event_WeaponDrop); HookEvent("player_first_spawn", Event_PlayerFirstSpawn); + HookEvent("map_transition", Event_MapTransition); + HookEvent("player_death", Event_PlayerDeath); + campaignStartTime = GetTime(); for(int i = 1; i <= MaxClients; i++) { if(IsClientConnected(i) && IsClientInGame(i)) { + playerJoinTime[i] = GetTime(); OnClientPutInServer(i); } } - TryStartTimer(true); - AutoExecConfig(true, "adminpanel"); - RegAdminCmd("sm_panel_status", Command_PanelStatus, ADMFLAG_GENERIC); - + RegAdminCmd("sm_panel_debug", Command_PanelDebug, ADMFLAG_GENERIC); + RegAdminCmd("sm_panel_request_stop", Command_RequestStop, ADMFLAG_GENERIC); } -#define DATE_FORMAT "%F at %I:%M %p" -Action Command_PanelStatus(int client, int args) { - ReplyToCommand(client, "Active: %b", updateTimer != null); - ReplyToCommand(client, "#Players: %d", numberOfPlayers); - ReplyToCommand(client, "Update Interval: %0f s", fastUpdateMode ? UPDATE_INTERVAL : UPDATE_INTERVAL_SLOW); - char buffer[32]; - ReplyToCommand(client, "Last Error Code: %d", lastErrorCode); - if(lastSuccessTime > 0) - FormatTime(buffer, sizeof(buffer), DATE_FORMAT, lastSuccessTime); - else - Format(buffer, sizeof(buffer), "(none)"); - ReplyToCommand(client, "Last Success: %s", buffer); - return Plugin_Handled; +void TriggerHealthUpdate(int client, bool instant = false) { + if(updateHealthTimer[client] != null) { + delete updateHealthTimer[client]; + } + updateHealthTimer[client] = CreateTimer(instant ? 0.1 : 1.0, Timer_UpdateHealth, client); } -void TryStartTimer(bool fast = true) { - if(numberOfPlayers > 0 && updateTimer == null && postAddress[0] != '\0' && authKey[0] != 0) { - fastUpdateMode = fast; - float interval = fast ? UPDATE_INTERVAL : UPDATE_INTERVAL_SLOW; - updateTimer = CreateTimer(interval, Timer_PostStatus, _, TIMER_REPEAT); - PrintToServer("[AdminPanel] Updating every %.1f seconds", interval); +void TriggerItemUpdate(int client) { + if(updateItemTimer[client] != null) { + delete updateItemTimer[client]; + } + updateItemTimer[client] = CreateTimer(1.0, Timer_UpdateItems, client); +} + +void OnSocketError(Socket socket, int errorType, int errorNumber, int any) { + PrintToServer("[AdminPanel] Socket Error %d %d", errorType, errorNumber); + if(!socket.Connected) { + PrintToServer("[AdminPanel] Lost connection to socket, reconnecting", errorType, errorNumber); + ConnectSocket(); } } +void OnSocketReceive(Socket socket, const char[] receiveData, int dataSize, int arg) { + receiveBuffer.FromArray(receiveData, dataSize); + LiveRecordResponse response = view_as(receiveBuffer.ReadByte()); + if(cvar_debug.BoolValue) { + PrintToServer("[AdminPanel] Received: %d", response); + } + lastReceiveTime = GetTime(); + switch(response) { + case Live_OK: { + int viewerCount = receiveBuffer.ReadByte(); + g_isPaused = viewerCount == 0; + } + case Live_Reconnect: + CreateTimer(5.0, Timer_Reconnect); + case Live_Refresh: { + PrintToServer("[AdminPanel] Refresh requested, performing"); + StartPayload(); + AddGameRecord(); + SendPayload(); + + SendPlayers(); + } + } + if(receiveTimeoutTimer != null) { + delete receiveTimeoutTimer; + } + receiveTimeoutTimer = CreateTimer(SOCKET_TIMEOUT_DURATION, Timer_Reconnect, 1); +} + +void OnSocketConnect(Socket socket, int any) { + if(cvar_debug.BoolValue) + PrintToServer("[AdminPanel] Connected to %s:%d", serverIp, serverPort); + g_socket.SetArg(0); + // Late loads / first setup we can't send + if(currentMap[0] != '\0' && StartPayload()) { + AddGameRecord(); + SendPayload(); + // Resend all players + SendPlayers(); + } +} + +void OnSocketDisconnect(Socket socket, int attempt) { + g_socket.SetArg(attempt + 1); + float nextAttempt = Exponential(float(attempt) / 2.0) + 2.0; + if(nextAttempt > MAX_ATTEMPT_TIMEOUT) nextAttempt = MAX_ATTEMPT_TIMEOUT; + PrintToServer("[AdminPanel] Disconnected, retrying in %.0f seconds", nextAttempt); + CreateTimer(nextAttempt, Timer_Reconnect); +} + +Action Timer_Reconnect(Handle h, int type) { + if(type == 1) { + PrintToServer("[AdminPanel] No response after %f seconds, attempting reconnect", SOCKET_TIMEOUT_DURATION); + } + ConnectSocket(); + return Plugin_Handled; +} + +void ConnectSocket() { + if(g_socket == null) LogError("Socket is invalid"); + if(g_socket.Connected) + g_socket.Disconnect(); + if(serverId[0] == '\0') return; + g_socket.SetOption(DebugMode, cvar_debug.BoolValue); + g_socket.Connect(OnSocketConnect, OnSocketReceive, OnSocketDisconnect, serverIp, serverPort); +} + +#define DATE_FORMAT "%F at %I:%M %p" +Action Command_PanelDebug(int client, int args) { + char arg[32]; + GetCmdArg(1, arg, sizeof(arg)); + if(StrEqual(arg, "connect")) { + if(serverId[0] == '\0') + ReplyToCommand(client, "No server id."); + else + ConnectSocket(); + } else if(StrEqual(arg, "info")) { + ReplyToCommand(client, "Connected: %b\tPaused: %b\t#Player: %d", g_socket.Connected, g_isPaused, numberOfPlayers); + ReplyToCommand(client, "ID: %s", serverId); + ReplyToCommand(client, "Target Host: %s:%d", serverIp, serverPort); + ReplyToCommand(client, "Buffer Size: %d", BUFFER_SIZE); + } else if(g_socket.Connected) { + if(StrEqual(arg, "game")) { + StartPayload(); + AddGameRecord(); + SendPayload(); + } else if(StrEqual(arg, "players")) { + SendPlayers(); + } else { + ReplyToCommand(client, "Unknown type"); + return Plugin_Handled; + } + } else { + ReplyToCommand(client, "Not connected"); + } + return Plugin_Handled; +} + +Action Command_RequestStop(int client, int args) { + if(GetClientCount(false) > 0) { + ReplyToCommand(client, "There are still %d players online.", GetClientCount(false)); + } else { + ReplyToCommand(client, "Stopping..."); + RequestFrame(StopServer); + } + return Plugin_Handled; +} +void StopServer() { + ServerCommand("exit"); +} void Event_GameStart(Event event, const char[] name, bool dontBroadcast) { campaignStartTime = GetTime(); + if(StartPayload()) { + AddGameRecord(); + SendPayload(); + } } void Event_GameEnd(Event event, const char[] name, bool dontBroadcast) { campaignStartTime = 0; } -void Event_HealStart(Event event, const char[] name, bool dontBroadcast) { - int healing = GetClientOfUserId(event.GetInt("subject")); - g_icBeingHealed[healing] = true; +void Event_MapTransition(Event event, const char[] name, bool dontBroadcast) { + g_inTransition = true; + if(StartPayload()) { + AddGameRecord(); + SendPayload(); + } } -void Event_HealStop(Event event, const char[] name, bool dontBroadcast) { - int healing = GetClientOfUserId(event.GetInt("subject")); - g_icBeingHealed[healing] = false; + +void Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast) { + int client = GetClientOfUserId(event.GetInt("userid")); + if(client > 0) { + PrintToServer("death: %N", client); + TriggerHealthUpdate(client, true); + } +} + +void Event_HealStart(Event event, const char[] name, bool dontBroadcast) { + int subject = GetClientOfUserId(event.GetInt("subject")); + g_icBeingHealed[subject] = true; +} +void Event_HealSuccess(Event event, const char[] name, bool dontBroadcast) { + int healer = GetClientOfUserId(event.GetInt("userid")); + int subject = GetClientOfUserId(event.GetInt("subject")); + if(subject > 0 && StartPayload()) { + g_icBeingHealed[subject] = false; + // Update the subject's health: + AddSurvivorRecord(subject); + // Update the teammate who healed subject: + AddSurvivorItemsRecord(healer); + SendPayload(); + } +} +void Event_HealInterrupted(Event event, const char[] name, bool dontBroadcast) { + int subject = GetClientOfUserId(event.GetInt("subject")); + g_icBeingHealed[subject] = false; } void Event_PlayerFirstSpawn(Event event, const char[] name, bool dontBroadcast) { @@ -141,24 +315,155 @@ void RecalculatePlayerCount() { numberOfPlayers = players; } +void SendPlayers() { + for(int i = 1; i <= MaxClients; i++) { + if(IsClientInGame(i)) { + StartPayload(); + AddPlayerRecord(i); + SendPayload(); + } + } +} + +// public void OnPlayerRunCmdPost(int client, int buttons, int impulse, const float vel[3], const float angles[3], int weapon, int subtype, int cmdnum, int tickcount, int seed, const int mouse[2]) { +// float time = GetGameTime(); +// // if(time - lastUpdateTime[client] > 7.0) { +// // if(StartPayload()) { +// // lastUpdateTime[client] = time; +// // AddSurvivorRecord(client); +// // SendPayload(); +// // } +// // } +// } + public void OnMapStart() { GetCurrentMap(currentMap, sizeof(currentMap)); numberOfPlayers = 0; + if(lateLoaded) { + StartPayload(); + AddGameRecord(); + SendPayload(); + + SendPlayers(); + } } +public void OnConfigsExecuted() { + isL4D1Survivors = L4D2_GetSurvivorSetMap() == 1; +} // Player counts public void OnClientPutInServer(int client) { + if(g_inTransition) { + g_inTransition = false; + if(StartPayload()) { + AddGameRecord(); + SendPayload(); + } + } GetClientName(client, nameCache[client], MAX_NAME_LENGTH); if(!IsFakeClient(client)) { GetClientAuthId(client, AuthId_SteamID64, steamidCache[client], 32); numberOfPlayers++; - TryStartTimer(true); } else { + // Check if they are not a bot, such as ABMBot or EPIBot, etc + char classname[32]; + GetEntityClassname(client, classname, sizeof(classname)); + if(StrContains(classname, "bot", false) > -1) { + return; + } strcopy(steamidCache[client], 32, "BOT"); } + SDKHook(client, SDKHook_WeaponEquipPost, OnWeaponPickUp); + SDKHook(client, SDKHook_OnTakeDamageAlivePost, OnTakeDamagePost); + // We wait a frame because Event_PlayerFirstSpawn sets their join time + RequestFrame(SendNewClient, client); +} + +void OnWeaponPickUp(int client, int weapon) { + // float time = GetGameTime(); + // if(time - lastUpdateTime[client] > 3.0 && StartPayload()) { + // lastUpdateTime[client] = time; + // AddSurvivorItemsRecord(client); + // SendPayload(); + // } + if(GetClientTeam(client) == 2) + TriggerItemUpdate(client); +} + +// Tracks the inventories for pills/adr used, kit used, ammo pack used, etc +void Event_WeaponDrop(Event event, const char[] name, bool dontBroadcast) { + int client = GetClientOfUserId(event.GetInt("userid")); + if(client > 0) { + if(GetClientTeam(client) == 2) + TriggerItemUpdate(client); + // if(StartPayload()) { + // AddSurvivorItemsRecord(client); + // SendPayload(); + // } + } +} + +void Event_ItemUsed(Event event, const char[] name, bool dontBroadcast) { + int client = GetClientOfUserId(event.GetInt("userid")); + if(client > 0) { + // if(StartPayload()) { + // AddSurvivorRecord(client); + // SendPayload(); + // } + if(GetClientTeam(client) == 2) + TriggerHealthUpdate(client); + } +} + +void OnTakeDamagePost(int victim, int attacker, int inflictor, float damage, int damagetype, int weapon, const float damageForce[3], const float damagePosition[3], int damagecustom) { + if(damage > 1.0 && victim > 0 && victim <= MaxClients) { + TriggerHealthUpdate(victim); + // if(GetGameTime() - lastUpdateTime[victim] > 0.3 && StartPayload()) { + // lastUpdateTime[victim] = GetGameTime(); + // if(GetClientTeam(victim) == 2) + // AddSurvivorRecord(victim); + // else + // AddInfectedRecord(victim); + // SendPayload(); + // } + } +} + +Action Timer_UpdateHealth(Handle h, int client) { + if(IsClientInGame(client) && StartPayload()) { + if(GetClientTeam(client) == 2) + AddSurvivorRecord(client); + else + AddInfectedRecord(client); + SendPayload(); + } + updateHealthTimer[client] = null; + return Plugin_Handled; +} +Action Timer_UpdateItems(Handle h, int client) { + if(IsClientInGame(client) && StartPayload()) { + AddSurvivorItemsRecord(client); + SendPayload(); + } + updateItemTimer[client] = null; + return Plugin_Handled; +} + +void SendNewClient(int client) { + if(!IsClientInGame(client)) return; + if(StartPayload()) { + PrintToServer("SendNewClient(%N)", client); + AddPlayerRecord(client); + SendPayload(); + } } public void OnClientDisconnect(int client) { + if(StartPayload()) { + // hopefully userid is valid here? + AddPlayerRecord(client, false); + SendPayload(); + } steamidCache[client][0] = '\0'; nameCache[client][0] = '\0'; if(!IsFakeClient(client)) { @@ -167,134 +472,58 @@ public void OnClientDisconnect(int client) { if(numberOfPlayers < 0) { numberOfPlayers = 0; } - if(numberOfPlayers == 0 && updateTimer != null) { - delete updateTimer; - } + } + if(updateHealthTimer[client] != null) { + delete updateHealthTimer[client]; + } + if(updateItemTimer[client] != null) { + delete updateItemTimer[client]; } } // Cvar updates void OnCvarChanged(ConVar convar, const char[] oldValue, const char[] newValue) { - if(cvar_postAddress == convar) { - strcopy(postAddress, sizeof(postAddress), newValue); - PrintToServer("[AdminPanel] Update Url has updated"); - } else if(cvar_authKey == convar) { - strcopy(authKey, sizeof(authKey), newValue); - PrintToServer("[AdminPanel] Auth key has been updated"); + if(cvar_id == convar) { + strcopy(serverId, sizeof(serverId), newValue); + PrintToServer("[AdminPanel] Server ID changed to: %s", serverId); + } else if(cvar_address == convar) { + if(newValue[0] == '\0') { + if(g_socket.Connected) + g_socket.Disconnect(); + serverPort = DEFAULT_SERVER_PORT; + serverIp = "127.0.0.1"; + PrintToServer("[AdminPanel] Deactivated"); + } else { + int index = SplitString(newValue, ":", serverIp, sizeof(serverIp)); + if(index > -1) { + serverPort = StringToInt(newValue[index]); + if(serverPort == 0) serverPort = DEFAULT_SERVER_PORT; + } + PrintToServer("[AdminPanel] Sending data to %s:%d", serverIp, serverPort); + ConnectSocket(); + } } else if(cvar_gamemode == convar) { strcopy(gamemode, sizeof(gamemode), newValue); - } - TryStartTimer(true); -} - -bool isSubmitting; -Action Timer_PostStatus(Handle h) { - if(isSubmitting) return Plugin_Continue; - isSubmitting = true; - // TODO: optimize only if someone is requesting live - HTTPRequest req = new HTTPRequest(postAddress); - JSONObject obj = GetObject(); - req.SetHeader("x-authtoken", authKey); - // req.AppendFormParam("playerCount", "%d", numberOfPlayers); - // req.AppendFormParam("map", currentMap); - if(cvar_debug.BoolValue) PrintToServer("[AdminPanel] Submitting"); - req.Post(obj, Callback_PostStatus); - delete obj; - // req.PostForm(Callback_PostStatus); - return Plugin_Continue; -} - -void Callback_PostStatus(HTTPResponse response, any value, const char[] error) { - isSubmitting = false; - if(response.Status == HTTPStatus_NoContent || response.Status == HTTPStatus_OK) { - lastErrorCode = 0; - lastSuccessTime = GetTime(); - if(cvar_debug.BoolValue) - PrintToServer("[AdminPanel] Response: OK/204"); - // We have subscribers, kill timer and recreate it in fast mode (if not already): - if(!fastUpdateMode) { - PrintToServer("[AdminPanel] Switching to fast update interval for active viewers."); - if(updateTimer != null) - delete updateTimer; - TryStartTimer(true); + if(StartPayload()) { + AddGameRecord(); + SendPayload(); } - - } else if(response.Status == HTTPStatus_Gone) { - lastErrorCode = 0; - // We have no subscribers, kill timer and recreate it in slow mode (if not already): - if(fastUpdateMode) { - PrintToServer("[AdminPanel] Switching to slow update interval, no viewers"); - if(updateTimer != null) - delete updateTimer; - TryStartTimer(false); - } - } else { - lastErrorCode = view_as(response.Status); - lastSuccessTime = 0; - // TODO: backoff - PrintToServer("[AdminPanel] Getting response: %d", response.Status); - if(cvar_debug.BoolValue) { - char buffer[64]; - JSONObject json = view_as(response.Data); - if(json.GetString("error", buffer, sizeof(buffer))) { - PrintToServer("[AdminPanel] Got %d response from server: \"%s\"", view_as(response.Status), buffer); - json.GetString("message", buffer, sizeof(buffer)); - PrintToServer("[AdminPanel] Error message: \"%s\"", buffer); - } else { - PrintToServer("[AdminPanel] Got %d response from server: \n%s", view_as(response.Status), error); - } - } - if(view_as(response.Status) == 0 || response.Status == HTTPStatus_Unauthorized || response.Status == HTTPStatus_Forbidden) { - PrintToServer("[AdminPanel] API Key seems to be invalid, killing timer."); - if(updateTimer != null) - delete updateTimer; + } else if(cvar_difficulty == convar) { + gameDifficulty = GetDifficultyInt(); + if(StartPayload()) { + AddGameRecord(); + SendPayload(); } } } -JSONObject GetObject() { - JSONObject obj = new JSONObject(); - obj.SetInt("playerCount", numberOfPlayers); - obj.SetString("map", currentMap); - obj.SetString("gamemode", gamemode); - obj.SetInt("startTime", uptime); - obj.SetFloat("fps", 1.0 / GetGameFrameTime()); - AddFinaleInfo(obj); - JSONArray players = GetPlayers(); - obj.Set("players", players); - delete players; - obj.SetFloat("refreshInterval", UPDATE_INTERVAL); - obj.SetInt("lastUpdateTime", GetTime()); - obj.SetInt("campaignStartTime", campaignStartTime); - return obj; -} - -void AddFinaleInfo(JSONObject parentObj) { - if(L4D_IsMissionFinalMap()) { - JSONObject obj = new JSONObject(); - obj.SetBool("escapeLeaving", L4D_IsFinaleEscapeInProgress()); - obj.SetInt("finaleStage", L4D2_GetCurrentFinaleStage()); - parentObj.Set("finaleInfo", obj); - delete obj; +public void L4D2_OnChangeFinaleStage_Post(int finaleType, const char[] arg) { + if(StartPayload()) { + AddFinaleRecord(finaleType); + SendPayload(); } } -JSONArray GetPlayers() { - JSONArray players = new JSONArray(); - for(int i = 1; i <= MaxClients; i++) { - if(IsClientConnected(i) && IsClientInGame(i)) { - int team = GetClientTeam(i); - if( team == 2 || team == 3) { - JSONObject player = GetPlayer(i); - players.Push(player); - delete player; - } - } - } - return players; -} - - enum { Action_BeingHealed = -1, Action_None = 0, // No use action active @@ -351,7 +580,9 @@ enum { pState_BlackAndWhite = 1, pState_InSaferoom = 2, pState_IsCalm = 4, - pState_IsBoomed = 8 + pState_IsBoomed = 8, + pState_IsPinned = 16, + pState_IsAlive = 32, } stock bool IsPlayerBoomed(int client) { @@ -365,62 +596,262 @@ int GetPlayerStates(int client) { if(GetEntProp(client, Prop_Send, "m_bIsOnThirdStrike", 1)) state |= pState_BlackAndWhite; if(GetEntProp(client, Prop_Send, "m_isCalm")) state |= pState_IsCalm; if(IsPlayerBoomed(client)) state |= pState_IsBoomed; + if(IsPlayerAlive(client)) state |= pState_IsAlive; + if(L4D2_GetInfectedAttacker(client) > 0) state |= pState_IsPinned; return state; } -JSONObject GetPlayer(int client) { - int team = GetClientTeam(client); - JSONObject player = new JSONObject(); - player.SetString("steamid", steamidCache[client]); - player.SetInt("userId", GetClientUserId(client)); - player.SetString("name", nameCache[client]); - player.SetInt("team", team); - player.SetBool("isAlive", IsPlayerAlive(client)); - player.SetInt("joinTime", playerJoinTime[client]); - player.SetInt("permHealth", GetEntProp(client, Prop_Send, "m_iHealth")); - if(team == 2) { - // Include idle players (player here is their idle bot) - if(IsFakeClient(client)) { - int idlePlayer = L4D_GetIdlePlayerOfBot(client); - if(idlePlayer > 0) { - player.SetString("idlePlayerId", steamidCache[idlePlayer]); - if(IsClientInGame(idlePlayer)) { - JSONObject idlePlayerObj = GetPlayer(idlePlayer); - player.Set("idlePlayer", idlePlayerObj); - delete idlePlayerObj; - } - } - } - player.SetInt("action", GetAction(client)); - player.SetInt("flowProgress", L4D2_GetVersusCompletionPlayer(client)); - player.SetFloat("flow", L4D2Direct_GetFlowDistance(client)); - player.SetBool("isPinned", L4D2_GetInfectedAttacker(client) > 0); - player.SetInt("tempHealth", L4D_GetPlayerTempHealth(client)); - player.SetInt("states", GetPlayerStates(client)); - player.SetInt("move", GetPlayerMovement(client)); - player.SetInt("survivor", GetEntProp(client, Prop_Send, "m_survivorCharacter")); - JSONArray weapons = GetPlayerWeapons(client); - player.Set("weapons", weapons); - delete weapons; - } else if(team == 3) { - player.SetInt("class", L4D2_GetPlayerZombieClass(client)); - player.SetInt("maxHealth", GetEntProp(client, Prop_Send, "m_iMaxHealth")); - int victim = L4D2_GetSurvivorVictim(client); - if(victim > 0) - player.SetString("pinnedSurvivorId", steamidCache[victim]); - } - return player; +stock int GetDifficultyInt() { + char diff[16]; + cvar_difficulty.GetString(diff, sizeof(diff)); + if(StrEqual(diff, "easy", false)) return 0; + else if(StrEqual(diff, "hard", false)) return 2; + else if(StrEqual(diff, "impossible", false)) return 3; + else return 1; } -JSONArray GetPlayerWeapons(int client) { - JSONArray weapons = new JSONArray(); - static char buffer[64]; - for(int slot = 0; slot < 6; slot++) { - if(GetClientWeaponNameSmart(client, slot, buffer, sizeof(buffer))) { - weapons.PushString(buffer); - } else { - weapons.PushNull(); +enum LiveRecordType { + Live_Game, + Live_Player, + Live_Survivor, + Live_Infected, + Live_Finale, + Live_SurvivorItems, + Live_CommandResponse, + Live_Auth +} + +enum LiveRecordResponse { + Live_OK, + Live_Reconnect, + Live_Error, + Live_Refresh, + Live_RunComand +} + +bool StartPayload() { + if(!cvar_debug.BoolValue && (g_isPaused || numberOfPlayers == 0)) return false; + sendBuffer.Reset(); + sendBuffer.WriteByte(LIVESTATUS_VERSION); + sendBuffer.WriteString(serverId); + return g_socket.Connected; +} + +void StartRecord(LiveRecordType type) { + sendBuffer.WriteChar('\x1e'); // record separator + sendBuffer.WriteByte(view_as(type)); +} + +void AddGameRecord() { + PrintToServer("pushing Live_Game"); + StartRecord(Live_Game); + sendBuffer.WriteInt(uptime); + sendBuffer.WriteInt(campaignStartTime); + sendBuffer.WriteByte(gameDifficulty); + sendBuffer.WriteByte(g_inTransition); + sendBuffer.WriteString(gamemode); + sendBuffer.WriteString(currentMap); +} + +void AddFinaleRecord(int stage) { + StartRecord(Live_Finale); + sendBuffer.WriteByte(stage); // finale stage + sendBuffer.WriteByte(L4D_IsFinaleEscapeInProgress()); // escape or not +} + +void AddPlayerRecord(int client, bool connected = true) { + // fake bots are ignored: + + int originalClient = client; + bool isIdle = false; + if(connected) { + // If this is an idle player's bot, then we use the real player's info instead. + if(IsFakeClient(client)) { + int realPlayer = L4D_GetIdlePlayerOfBot(client); + if(realPlayer > 0) { + PrintToServer("%d is idle bot of %N", client, realPlayer); + isIdle = true; + client = realPlayer; + } else if(steamidCache[client][0] == '\0') { + PrintToServer("skipping %N %s", client, steamidCache[client]); + return; + } } } - return weapons; + StartRecord(Live_Player); + sendBuffer.WriteInt(GetClientUserId(client)); + sendBuffer.WriteString(steamidCache[client]); + if(connected) { + sendBuffer.WriteByte(isIdle); + sendBuffer.WriteInt(playerJoinTime[client]); + sendBuffer.WriteString(nameCache[client]); + + if(GetClientTeam(originalClient) == 2) { + AddSurvivorRecord(originalClient, client); + AddSurvivorItemsRecord(originalClient, client); + } else if(GetClientTeam(client) == 3) { + AddInfectedRecord(client); + } + } +} + +void AddSurvivorRecord(int client, int forClient = 0) { + if(forClient == 0) forClient = client; + int survivor = GetEntProp(client, Prop_Send, "m_survivorCharacter"); + // The icons are mapped for survivors as 4,5,6,7; so inc to that for L4D1 survivors + if(isL4D1Survivors) { + survivor += 4; + } + if(survivor >= 8) return; + StartRecord(Live_Survivor); + sendBuffer.WriteInt(GetClientUserId(forClient)); + sendBuffer.WriteByte(survivor); + sendBuffer.WriteByte(L4D_GetPlayerTempHealth(client)); //temp health + sendBuffer.WriteByte(GetEntProp(client, Prop_Send, "m_iHealth")); //perm health + sendBuffer.WriteByte(L4D2_GetVersusCompletionPlayer(client)); // flow% + sendBuffer.WriteInt(GetPlayerStates(client)); // state (incl. alive) + sendBuffer.WriteInt(GetPlayerMovement(client)); // move + sendBuffer.WriteInt(GetAction(client)); // action +} +void AddSurvivorItemsRecord(int client, int forClient = 0) { + if(forClient == 0) forClient = client; + StartRecord(Live_SurvivorItems); + sendBuffer.WriteInt(GetClientUserId(client)); + char name[32]; + for(int slot = 0; slot < 6; slot++) { + name[0] = '\0'; + GetClientWeaponNameSmart2(client, slot, name, sizeof(name)); + sendBuffer.WriteString(name); + } +} +void AddInfectedRecord(int client) { + StartRecord(Live_Infected); + sendBuffer.WriteInt(GetClientUserId(client)); + sendBuffer.WriteShort(GetEntProp(client, Prop_Send, "m_iHealth")); //cur health + sendBuffer.WriteShort(GetEntProp(client, Prop_Send, "m_iMaxHealth")); //max health + sendBuffer.WriteByte(L4D2_GetPlayerZombieClass(client)); // class + int victim = L4D2_GetSurvivorVictim(client); + if(victim > 0) + sendBuffer.WriteInt(GetClientUserId(victim)); + else + sendBuffer.WriteInt(0); +} + +void SendPayload() { + sendBuffer.Finish(); + if(cvar_debug.BoolValue) + PrintToServer("[AdminPanel] Sending %d bytes of data", sendBuffer.offset); + g_socket.Send(sendBuffer.buffer, sendBuffer.offset); +} + +enum struct Buffer { + char buffer[BUFFER_SIZE]; + int offset; + + void Reset() { + this.buffer[0] = '\0'; + this.offset = 0; + } + + void FromArray(const char[] input, int size) { + this.Reset(); + int max = BUFFER_SIZE; + if(size < max) max = size; + for(int i = 0; i < max; i++) { + this.buffer[i] = input[i]; + } + } + + void Print() { + char[] output = new char[BUFFER_SIZE+100]; + for(int i = 0; i < BUFFER_SIZE; i++) { + if(this.buffer[i] == '\0') { + Format(output, BUFFER_SIZE, "%s \\0", output); + } else { + Format(output, BUFFER_SIZE, "%s %c", output, this.buffer[i]); + } + } + PrintToServer("%s", output); + } + + void WriteChar(char c) { + this.buffer[this.offset++] = c; + } + + void WriteByte(int value) { + this.buffer[this.offset++] = value & 0xFF; + } + + void WriteShort(int value) { + this.buffer[this.offset++] = value & 0xFF; + this.buffer[this.offset++] = (value >> 8) & 0xFF; + } + + void WriteInt(int value, int bytes = 4) { + this.buffer[this.offset++] = value & 0xFF; + this.buffer[this.offset++] = (value >> 8) & 0xFF; + this.buffer[this.offset++] = (value >> 16) & 0xFF; + this.buffer[this.offset++] = (value >> 24) & 0xFF; + } + + void WriteFloat(float value) { + this.WriteInt(view_as(value)); + } + + /// Writes a variable-width string, with the size being prepended. Only supports strings up to 2^15 in size + /// @param lenHint - optional, but the length of the string, to avoid strlen() twice + void WriteVarString(const char[] string, int lenHint = -1) { + if(lenHint < 0) lenHint = strlen(string); + this.WriteShort(lenHint); + // null term written will just get overwritten + strcopy(this.buffer[this.offset], BUFFER_SIZE, string); + this.offset += lenHint; + } + + // Writes a null-terminated length string, strlen > size is truncated. + void WriteString(const char[] string) { + int written = strcopy(this.buffer[this.offset], BUFFER_SIZE, string); + this.offset += written + 1; + } + + void Finish() { + // Set newline + this.buffer[this.offset++] = '\n'; + } + + int ReadByte() { + return this.buffer[this.offset++] & 0xFF; + } + + int ReadShort() { + int value = this.buffer[this.offset++]; + value += this.buffer[this.offset++] << 8; + return value; + } + + int ReadInt() { + int value = this.buffer[this.offset++]; + value += this.buffer[this.offset++] << 8; + value += this.buffer[this.offset++] << 16; + value += this.buffer[this.offset++] << 32; + return value; + } + + float ReadFloat() { + return view_as(this.ReadInt()); + } + + int ReadString(char[] output, int maxlen) { + int len = strcopy(output, maxlen, this.buffer[this.offset]) + 1; + this.offset += len; + return len; + } + + char ReadChar() { + return this.buffer[this.offset++]; + } + + bool EOF() { + return this.offset >= BUFFER_SIZE; + } } \ No newline at end of file