sourcemod-plugins/scripting/adminpanel.sp

416 lines
No EOL
13 KiB
SourcePawn

#pragma semicolon 1
#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
#include <sourcemod>
#include <sdktools>
#include <ripext>
#include <left4dhooks>
#include <jutils>
#pragma newdecls required
public Plugin myinfo =
{
name = "Admin Panel",
author = "Jackz",
description = "Plugin to integrate with admin panel",
version = "1.0.0",
url = "https://github.com/jackzmc/l4d2-admin-dash"
};
ConVar cvar_debug;
ConVar cvar_postAddress; char postAddress[128];
ConVar cvar_authKey; char authKey[512];
ConVar cvar_gamemode; char gamemode[32];
char currentMap[64];
int numberOfPlayers = 0;
int lastSuccessTime;
int campaignStartTime;
int lastErrorCode;
int uptime;
bool fastUpdateMode = false;
Handle updateTimer = null;
char steamidCache[MAXPLAYERS+1][32];
char nameCache[MAXPLAYERS+1][MAX_NAME_LENGTH];
int g_icBeingHealed[MAXPLAYERS+1];
int playerJoinTime[MAXPLAYERS+1];
public void OnPluginStart()
{
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_gamemode = FindConVar("mp_gamemode");
cvar_gamemode.AddChangeHook(OnCvarChanged);
cvar_gamemode.GetString(gamemode, sizeof(gamemode));
HookEvent("game_init", Event_GameStart);
HookEvent("heal_success", Event_HealStop);
HookEvent("heal_interrupted", Event_HealStop);
HookEvent("player_first_spawn", Event_PlayerFirstSpawn);
for(int i = 1; i <= MaxClients; i++) {
if(IsClientConnected(i) && IsClientInGame(i)) {
OnClientPutInServer(i);
}
}
TryStartTimer(true);
AutoExecConfig(true, "adminpanel");
RegAdminCmd("sm_panel_status", Command_PanelStatus, 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 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 Event_GameStart(Event event, const char[] name, bool dontBroadcast) {
campaignStartTime = GetTime();
}
void Event_HealStart(Event event, const char[] name, bool dontBroadcast) {
int healing = GetClientOfUserId(event.GetInt("subject"));
g_icBeingHealed[healing] = true;
}
void Event_HealStop(Event event, const char[] name, bool dontBroadcast) {
int healing = GetClientOfUserId(event.GetInt("subject"));
g_icBeingHealed[healing] = false;
}
void Event_PlayerFirstSpawn(Event event, const char[] name, bool dontBroadcast) {
int client = GetClientOfUserId(event.GetInt("userid"));
playerJoinTime[client] = GetTime();
RecalculatePlayerCount();
}
void RecalculatePlayerCount() {
int players = 0;
for(int i = 1; i <= MaxClients; i++) {
if(IsClientConnected(i) && IsClientInGame(i) && !IsFakeClient(i)) {
players++;
}
}
numberOfPlayers = players;
}
public void OnMapStart() {
GetCurrentMap(currentMap, sizeof(currentMap));
numberOfPlayers = 0;
}
// Player counts
public void OnClientPutInServer(int client) {
GetClientName(client, nameCache[client], MAX_NAME_LENGTH);
if(!IsFakeClient(client)) {
GetClientAuthId(client, AuthId_SteamID64, steamidCache[client], 32);
numberOfPlayers++;
TryStartTimer(true);
} else {
strcopy(steamidCache[client], 32, "BOT");
}
}
public void OnClientDisconnect(int client) {
steamidCache[client][0] = '\0';
nameCache[client][0] = '\0';
if(!IsFakeClient(client)) {
numberOfPlayers--;
// Incase somehow we lost track
if(numberOfPlayers < 0) {
numberOfPlayers = 0;
}
if(numberOfPlayers == 0 && updateTimer != null) {
delete updateTimer;
}
}
}
// 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");
} 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);
}
} 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
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(response.Status == HTTPStatus_Unauthorized || response.Status == HTTPStatus_Forbidden) {
PrintToServer("[AdminPanel] API Key seems to be invalid, killing timer.");
if(updateTimer != null)
delete updateTimer;
}
}
}
JSONObject GetObject() {
JSONObject obj = new JSONObject();
obj.SetInt("playerCount", numberOfPlayers);
obj.SetString("map", currentMap);
obj.SetString("gamemode", gamemode);
obj.SetInt("startTime", uptime);
obj.SetInt("commonsCount", L4D_GetCommonsCount());
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;
}
}
JSONArray GetPlayers() {
JSONArray players = new JSONArray();
for(int i = 1; i <= MaxClients; i++) {
if(IsClientConnected(i) && IsClientInGame(i) && GetClientTeam(i) >= 2) {
JSONObject player = GetPlayer(i);
players.Push(player);
delete player;
}
}
return players;
}
enum {
Action_BeingHealed = -1,
Action_None = 0, // No use action active
Action_Healing = 1, // Includes healing yourself or a teammate.
Action_AmmoPack = 2, // When deploying the ammo pack that was never added into the game
Action_Defibing = 4, // When defib'ing a dead body.
Action_GettingDefibed = 5, // When comming back to life from a dead body.
Action_DeployIncendiary = 6, // When deploying Incendiary ammo
Action_DeployExplosive = 7, // When deploying Explosive ammo
Action_PouringGas = 8, // Pouring gas into a generator
Action_Cola = 9, // For Dead Center map 2 cola event, when handing over the cola to whitalker.
Action_Button = 10, // Such as buttons, timed buttons, generators, etc.
Action_UsePointScript = 11 // When using a "point_script_use_target" entity
}
int GetAction(int client) {
if(g_icBeingHealed[client]) return Action_BeingHealed;
return view_as<int>(L4D2_GetPlayerUseAction(client));
}
enum {
Move_UnderAttack = -3,
Move_Hanging = -2,
Move_Incapped = -1,
Move_Idle = 0,
Move_Walk = 1,
Move_Run = 2,
Move_Crouched = 3,
Move_Ladder = 4
}
stock float GetPlayerSpeed(int client) {
int iVelocity = FindSendPropInfo("CTerrorPlayer", "m_vecVelocity[0]");
float velocity[3];
GetEntDataVector(client, iVelocity, velocity);
return GetVectorLength(velocity, false);
}
int GetPlayerMovement(int client) {
MoveType moveType = GetEntityMoveType(client);
if(moveType == MOVETYPE_LADDER) return Move_Ladder;
else if(GetEntProp(client, Prop_Send, "m_bDucked", 1)) return Move_Crouched;
else if(GetEntProp(client, Prop_Send, "m_isIncapacitated", 1)) return Move_Incapped;
else if(GetEntProp(client, Prop_Send, "m_isHangingFromLedge", 1)) return Move_Hanging;
else if(L4D_GetPinnedSurvivor(client)) return Move_UnderAttack; // TODO: optimize for events
float velocity = GetPlayerSpeed(client);
if(velocity > 85.0) return Move_Run;
else if(velocity > 1.0) return Move_Walk;
return Move_Idle;
}
// TODO: pursued by witch
enum {
pState_BlackAndWhite = 1,
pState_InSaferoom = 2,
pState_IsCalm = 4,
pState_IsBoomed = 8
}
stock bool IsPlayerBoomed(int client) {
return (GetEntPropFloat(client, Prop_Send, "m_vomitStart") + 20.1) > GetGameTime();
}
int GetPlayerStates(int client) {
int state = 0;
if(L4D_IsInLastCheckpoint(client) || L4D_IsInFirstCheckpoint(client))
state |= pState_InSaferoom;
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;
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;
}
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();
}
}
return weapons;
}