New socket system

This commit is contained in:
Jackzie 2024-03-28 12:13:03 -05:00
parent 001e8cdb52
commit 9007092afd
2 changed files with 647 additions and 216 deletions

Binary file not shown.

View file

@ -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 <sourcemod>
#include <sdktools>
@ -15,6 +13,7 @@
#include <left4dhooks>
#include <multicolors>
#include <jutils>
#include <socket>
#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<LiveRecordResponse>(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<int>(response.Status);
lastSuccessTime = 0;
// TODO: backoff
PrintToServer("[AdminPanel] Getting response: %d", response.Status);
if(cvar_debug.BoolValue) {
char buffer[64];
JSONObject json = view_as<JSONObject>(response.Data);
if(json.GetString("error", buffer, sizeof(buffer))) {
PrintToServer("[AdminPanel] Got %d response from server: \"%s\"", view_as<int>(response.Status), buffer);
json.GetString("message", buffer, sizeof(buffer));
PrintToServer("[AdminPanel] Error message: \"%s\"", buffer);
} else {
PrintToServer("[AdminPanel] Got %d response from server: <unknown json>\n%s", view_as<int>(response.Status), error);
}
}
if(view_as<int>(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<int>(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;
}
}
}
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);
}
}
return weapons;
}
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<int>(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<float>(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;
}
}