#include #include #pragma semicolon 1 #pragma newdecls required // A player who keeps moving the couch the same way is reported at most once per this many seconds. #define LOG_REPEAT_INTERVAL 2.0 // How much each press in the push scale menu changes sm_couch_push_scale. #define PUSH_SCALE_STEP 0.25 // So commands fail with a message instead of an unbound-native error while the core is unloaded. bool g_bCoreLoaded; // Who sees couch interactions in their console. On by default for anyone who can run sm_couchlog, // off for everyone else. Per slot, so it survives map changes. bool g_bLogging[MAXPLAYERS + 1]; // Per player: what was last reported, and when the same thing may be reported again. CouchMethod g_iLastMethod[MAXPLAYERS + 1]; char g_sLastWeapon[MAXPLAYERS + 1][64]; float g_fNextReport[MAXPLAYERS + 1]; public Plugin myinfo = { name = "Couch Admin", author = "marisakirisame.net", description = "Admin commands and menu for the ZE couch", version = "2.0", url = "https://marisakirisame.net" }; public void OnPluginStart() { RegAdminCmd("sm_couch", Command_Couch, ADMFLAG_CHANGEMAP, "Opens the couch menu"); RegAdminCmd("sm_spawncouch", Command_SpawnCouch, ADMFLAG_CHANGEMAP, "Spawns the couch where you are aiming"); RegAdminCmd("sm_bringcouch", Command_BringCouch, ADMFLAG_CHANGEMAP, "Brings the couch to where you are aiming"); RegAdminCmd("sm_gotocouch", Command_GotoCouch, ADMFLAG_CHANGEMAP, "Teleports you to the couch"); RegAdminCmd("sm_removecouch", Command_RemoveCouch, ADMFLAG_CHANGEMAP, "Removes the couch"); RegAdminCmd("sm_couchbeacon", Command_Beacon, ADMFLAG_CHANGEMAP, "sm_couchbeacon [0|1] - beacon rings on the couch; no argument toggles"); RegAdminCmd("sm_couchpistolonly", Command_PistolOnly, ADMFLAG_CHANGEMAP, "sm_couchpistolonly [0|1] - only pistols and knives move the couch; no argument toggles"); RegAdminCmd("sm_couchlog", Command_Log, ADMFLAG_CHANGEMAP, "sm_couchlog [0|1] - show who moves the couch and how in your console; no argument toggles"); RegAdminCmd("sm_couchhp", Command_Hp, ADMFLAG_CHANGEMAP, "sm_couchhp [hp] - sets the couch's HP; no argument shows it"); RegAdminCmd("sm_couchmaxhp", Command_MaxHp, ADMFLAG_CHANGEMAP, "sm_couchmaxhp - sets the couch's maximum HP"); // Late load: admins are already in, so give them their default. for (int client = 1; client <= MaxClients; client++) { if (IsClientInGame(client) && !IsFakeClient(client)) g_bLogging[client] = IsLoggingAdmin(client); } } // Admin access is only known once the admin check has run, so the default is set here rather // than on connect. public void OnClientPostAdminCheck(int client) { if (!IsFakeClient(client)) g_bLogging[client] = IsLoggingAdmin(client); } // Follows the command's access, so an admin override on sm_couchlog changes who gets the default. bool IsLoggingAdmin(int client) { return CheckCommandAccess(client, "sm_couchlog", ADMFLAG_CHANGEMAP); } // Game time restarts with the map. public void OnMapStart() { for (int i = 1; i <= MAXPLAYERS; i++) g_fNextReport[i] = 0.0; } public void OnClientDisconnect(int client) { g_bLogging[client] = false; g_fNextReport[client] = 0.0; } public void OnAllPluginsLoaded() { g_bCoreLoaded = LibraryExists("Couch"); } public void OnLibraryAdded(const char[] name) { if (StrEqual(name, "Couch")) g_bCoreLoaded = true; } public void OnLibraryRemoved(const char[] name) { if (StrEqual(name, "Couch")) g_bCoreLoaded = false; } // --------------------------------------------------------------------------------------------- // Commands // --------------------------------------------------------------------------------------------- public Action Command_Couch(int client, int args) { if (!RequireInGame(client) || !RequireCore(client)) return Plugin_Handled; ShowCouchMenu(client); return Plugin_Handled; } public Action Command_SpawnCouch(int client, int args) { if (!RequireInGame(client) || !RequireCore(client)) return Plugin_Handled; DoSpawn(client); return Plugin_Handled; } public Action Command_BringCouch(int client, int args) { if (!RequireInGame(client) || !RequireCore(client)) return Plugin_Handled; DoBring(client); return Plugin_Handled; } public Action Command_GotoCouch(int client, int args) { if (!RequireInGame(client) || !RequireCore(client)) return Plugin_Handled; DoGoto(client); return Plugin_Handled; } // Needs no position, so it also works from the server console. public Action Command_RemoveCouch(int client, int args) { if (!RequireCore(client)) return Plugin_Handled; DoRemove(client); return Plugin_Handled; } public Action Command_Beacon(int client, int args) { if (!RequireCore(client)) return Plugin_Handled; bool bEnable = !Couch_IsBeaconEnabled(); if (args >= 1) bEnable = (GetCmdArgInt(1) != 0); DoSetBeacon(client, bEnable); return Plugin_Handled; } public Action Command_PistolOnly(int client, int args) { if (!RequireCore(client)) return Plugin_Handled; bool bEnable = !Couch_IsPistolOnly(); if (args >= 1) bEnable = (GetCmdArgInt(1) != 0); DoSetPistolOnly(client, bEnable); return Plugin_Handled; } public Action Command_Hp(int client, int args) { if (!RequireCore(client)) return Plugin_Handled; if (Couch_GetCouch() == -1) { ReplyToCommand(client, PREFIX_PLAIN ... " There is no couch. Use sm_spawncouch first."); return Plugin_Handled; } if (args < 1) { char sHp[16], sMax[16]; FormatThousands(Couch_GetHp(), sHp, sizeof(sHp)); FormatThousands(Couch_GetMaxHp(), sMax, sizeof(sMax)); ReplyToCommand(client, PREFIX_PLAIN ... " Couch HP: %s/%s (couch HP is %s).", sHp, sMax, Couch_IsHpEnabled() ? "on" : "off"); return Plugin_Handled; } DoSetHp(client, GetCmdArgInt(1)); return Plugin_Handled; } public Action Command_MaxHp(int client, int args) { if (!RequireCore(client)) return Plugin_Handled; if (args < 1) { ReplyToCommand(client, PREFIX_PLAIN ... " Usage: sm_couchmaxhp "); return Plugin_Handled; } int hp = GetCmdArgInt(1); if (hp < 1) { ReplyToCommand(client, PREFIX_PLAIN ... " The maximum HP must be at least 1."); return Plugin_Handled; } DoSetMaxHp(client, hp); return Plugin_Handled; } public Action Command_Log(int client, int args) { if (!RequireInGame(client)) return Plugin_Handled; bool bEnable = !g_bLogging[client]; if (args >= 1) bEnable = (GetCmdArgInt(1) != 0); DoSetLogging(client, bEnable); return Plugin_Handled; } bool RequireInGame(int client) { if (client >= 1 && IsClientInGame(client)) return true; ReplyToCommand(client, PREFIX_PLAIN ... " This command can only be used in-game."); return false; } bool RequireCore(int client) { if (g_bCoreLoaded) return true; ReplyToCommand(client, PREFIX_PLAIN ... " The Couch core plugin is not loaded."); return false; } // --------------------------------------------------------------------------------------------- // Actions // --------------------------------------------------------------------------------------------- void DoSpawn(int client) { if (Couch_Spawn(client) == -1) { ReplyToCommand(client, PREFIX_PLAIN ... " Could not create the couch."); return; } ShowActivity2(client, PREFIX, "\x01Spawned the \x04couch\x01, it will respawn here each round until removed."); LogAction(client, -1, "\"%L\" spawned the couch.", client); } void DoBring(int client) { if (!Couch_Bring(client)) { ReplyToCommand(client, PREFIX_PLAIN ... " There is no couch to bring. Use sm_spawncouch first."); return; } ShowActivity2(client, PREFIX, "\x01Brought the \x04couch\x01."); LogAction(client, -1, "\"%L\" brought the couch.", client); } void DoGoto(int client) { if (!IsPlayerAlive(client)) { ReplyToCommand(client, PREFIX_PLAIN ... " You must be alive to go to the couch."); return; } if (!Couch_Goto(client)) { ReplyToCommand(client, PREFIX_PLAIN ... " There is no couch to go to. Use sm_spawncouch first."); return; } ShowActivity2(client, PREFIX, "\x01Teleported to the \x04couch\x01."); LogAction(client, -1, "\"%L\" teleported to the couch.", client); } void DoRemove(int client) { if (!Couch_Remove()) { ReplyToCommand(client, PREFIX_PLAIN ... " There is no couch to remove."); return; } ShowActivity2(client, PREFIX, "\x01Removed the \x04couch\x01."); LogAction(client, -1, "\"%L\" removed the couch.", client); } void DoToggleEffect(int client) { bool bEnable = !Couch_IsEffectEnabled(); Couch_SetEffectEnabled(bEnable); ShowActivity2(client, PREFIX, "\x01Turned the couch effect \x04%s\x01.", bEnable ? "on" : "off"); LogAction(client, -1, "\"%L\" turned the couch effect %s.", client, bEnable ? "on" : "off"); } void DoSetBeacon(int client, bool bEnable) { Couch_SetBeaconEnabled(bEnable); ShowActivity2(client, PREFIX, "\x01Turned the couch beacon \x04%s\x01.", bEnable ? "on" : "off"); LogAction(client, -1, "\"%L\" turned the couch beacon %s.", client, bEnable ? "on" : "off"); } // Announced to everyone so players know why their rifle stopped moving the couch. void DoSetPistolOnly(int client, bool bEnable) { Couch_SetPistolOnly(bEnable); ShowActivity2(client, PREFIX, "\x01Turned couch pistol-only mode \x04%s\x01.", bEnable ? "on" : "off"); LogAction(client, -1, "\"%L\" turned couch pistol-only mode %s.", client, bEnable ? "on" : "off"); } // Announced to everyone so players know to shoot the couch instead. void DoToggleUse(int client) { bool bAllow = !Couch_IsUseAllowed(); Couch_SetUseAllowed(bAllow); if (bAllow) ShowActivity2(client, PREFIX, "\x01Allowed pushing the \x04couch\x01 with USE."); else ShowActivity2(client, PREFIX, "\x01Disabled pushing the \x04couch\x01 with USE, it must be shot to move."); LogAction(client, -1, "\"%L\" %s couch use.", client, bAllow ? "allowed" : "disallowed"); } void DoSetHpEnabled(int client, bool bEnable) { Couch_SetHpEnabled(bEnable); if (bEnable) ShowActivity2(client, PREFIX, "\x01Turned couch HP \x04on\x01, zombies can break the couch now."); else ShowActivity2(client, PREFIX, "\x01Turned couch HP \x04off\x01."); LogAction(client, -1, "\"%L\" turned couch HP %s.", client, bEnable ? "on" : "off"); } // Read back afterwards so the announcement shows the clamped value. 0 breaks the couch. void DoSetHp(int client, int hp) { Couch_SetHp(hp); hp = Couch_GetHp(); char sHp[16], sMax[16]; FormatThousands(hp, sHp, sizeof(sHp)); FormatThousands(Couch_GetMaxHp(), sMax, sizeof(sMax)); ShowActivity2(client, PREFIX, "\x01Set the couch's \x04HP\x01 to \x04%s/%s\x01.", sHp, sMax); LogAction(client, -1, "\"%L\" set the couch's HP to %d.", client, hp); } void DoSetMaxHp(int client, int hp) { Couch_SetMaxHp(hp); hp = Couch_GetMaxHp(); char sHp[16], sMax[16]; FormatThousands(Couch_GetHp(), sHp, sizeof(sHp)); FormatThousands(hp, sMax, sizeof(sMax)); ShowActivity2(client, PREFIX, "\x01Set the couch's \x04maximum HP\x01 to \x04%s\x01, it is now at \x04%s/%s\x01.", sMax, sHp, sMax); LogAction(client, -1, "\"%L\" set the couch's maximum HP to %d.", client, hp); } // The tier cvars belong to the core plugin, so they are looked up by name. sName is "move" or "button". void DoChangeTier(int client, const char[] sName, int iDelta) { ConVar cvar = FindTierConVar(sName); if (cvar == null) return; int iTier = cvar.IntValue + iDelta; if (iTier < 0) iTier = 0; cvar.IntValue = iTier; if (iTier == 0) { ShowActivity2(client, PREFIX, "\x01Turned off the couch \x04%s\x01 tier requirement.", sName); LogAction(client, -1, "\"%L\" turned off the couch %s tier requirement.", client, sName); } else { ShowActivity2(client, PREFIX, "\x01Set the couch \x04%s\x01 tier requirement to \x04%d\x01.", sName, iTier); LogAction(client, -1, "\"%L\" set the couch %s tier requirement to %d.", client, sName, iTier); } } ConVar FindTierConVar(const char[] sName) { char sCvar[32]; FormatEx(sCvar, sizeof(sCvar), "sm_couch_%s_tier", sName); return FindConVar(sCvar); } // Also a core plugin cvar. Rounded to the step so repeated presses don't drift, and read back // afterwards so the announcement shows the value after the cvar's own bounds have clamped it. void DoSetPushScale(int client, float fScale) { ConVar cvar = FindConVar("sm_couch_push_scale"); if (cvar == null) return; fScale = RoundToNearest(fScale / PUSH_SCALE_STEP) * PUSH_SCALE_STEP; if (fScale < 0.0) fScale = 0.0; cvar.FloatValue = fScale; fScale = cvar.FloatValue; ShowActivity2(client, PREFIX, "\x01Set the couch \x04push scale\x01 to \x04%.2fx\x01.", fScale); LogAction(client, -1, "\"%L\" set the couch push scale to %.2f.", client, fScale); } // Only affects what this admin sees, so it is neither announced nor logged. The choice lasts // until they disconnect; on their next connect the log is on again. void DoSetLogging(int client, bool bEnable) { g_bLogging[client] = bEnable; if (bEnable) PrintToChat(client, PREFIX ... "\x01Couch log \x04on\x01, see your console for who moves the couch and how."); else PrintToChat(client, PREFIX ... "\x01Couch log \x04off\x01 until you reconnect."); } // --------------------------------------------------------------------------------------------- // Couch log // --------------------------------------------------------------------------------------------- public void Couch_OnCouchInteraction(int couch, int client, CouchMethod method, const char[] weapon) { // Grenades are rare and knock the couch far, so everyone is told. One blast is one report, so no // rate limit is needed. The userid is there so an admin can put it straight into sm_couchban. if (method == CouchMethod_Grenade) PrintToChatAll(PREFIX ... "%N\x01 (#%d) hit the couch with a \x04grenade", client, GetClientUserId(client)); if (!AnyoneLogging()) return; // Held fire and shotgun pellets would flood the console; a change of weapon or method is always shown. float fNow = GetGameTime(); if (fNow < g_fNextReport[client] && method == g_iLastMethod[client] && StrEqual(weapon, g_sLastWeapon[client])) return; g_iLastMethod[client] = method; strcopy(g_sLastWeapon[client], sizeof(g_sLastWeapon[]), weapon); g_fNextReport[client] = fNow + LOG_REPEAT_INTERVAL; // Plain text: the console doesn't render chat colour codes. char sHow[96]; switch (method) { case CouchMethod_Bullet: { if (weapon[0] != '\0') FormatEx(sHow, sizeof(sHow), "shot the couch with %s", weapon); else strcopy(sHow, sizeof(sHow), "shot the couch"); } case CouchMethod_Knife: strcopy(sHow, sizeof(sHow), "knifed the couch"); case CouchMethod_Grenade: strcopy(sHow, sizeof(sHow), "hit the couch with a grenade"); case CouchMethod_Use: strcopy(sHow, sizeof(sHow), "pushed the couch with USE"); default: FormatEx(sHow, sizeof(sHow), "moved the couch with %s", weapon[0] != '\0' ? weapon : "something"); } // The userid is there so it can go straight into sm_couchban. int userid = GetClientUserId(client); for (int i = 1; i <= MaxClients; i++) { if (!g_bLogging[i] || !IsClientInGame(i)) continue; PrintToConsole(i, PREFIX_PLAIN ... " %N (#%d) %s", client, userid, sHow); } } bool AnyoneLogging() { for (int i = 1; i <= MaxClients; i++) { if (g_bLogging[i]) return true; } return false; } // --------------------------------------------------------------------------------------------- // Menu // --------------------------------------------------------------------------------------------- // Rebuilt on every display so the labels always reflect the current state. void ShowCouchMenu(int client) { bool bHasCouch = (Couch_GetCouch() != -1); bool bPlaced = Couch_IsPlaced(); Menu menu = new Menu(MenuHandler_Couch); menu.SetTitle("Couch"); // With a couch out, bring is the way to move it; without one there is nothing to bring. menu.AddItem("spawn", "Spawn couch at crosshair", bHasCouch ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT); menu.AddItem("bring", "Bring couch to crosshair", bHasCouch ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED); menu.AddItem("goto", "Teleport to couch", bHasCouch ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED); if (bHasCouch) menu.AddItem("remove", "Remove couch"); else if (bPlaced) menu.AddItem("remove", "Remove couch (respawns next round)"); else menu.AddItem("remove", "Remove couch", ITEMDRAW_DISABLED); char sItem[64]; FormatEx(sItem, sizeof(sItem), "Pistol only: %s", Couch_IsPistolOnly() ? "ON" : "OFF"); menu.AddItem("pistol", sItem); FormatEx(sItem, sizeof(sItem), "Allow USE: %s", Couch_IsUseAllowed() ? "ON" : "OFF"); menu.AddItem("use", sItem); FormatEx(sItem, sizeof(sItem), "Couch HP: %s", Couch_IsHpEnabled() ? "ON" : "OFF"); menu.AddItem("hp", sItem); FormatEx(sItem, sizeof(sItem), "Couch logs (for me): %s", g_bLogging[client] ? "ON" : "OFF"); menu.AddItem("log", sItem); menu.AddItem("effects", "Effects"); menu.AddItem("tiers", "Tier requirements"); menu.AddItem("pushscale", "Push scale"); menu.ExitButton = true; menu.Display(client, MENU_TIME_FOREVER); } public int MenuHandler_Couch(Menu menu, MenuAction action, int param1, int param2) { switch (action) { case MenuAction_Select: { int client = param1; if (!g_bCoreLoaded) { RequireCore(client); return 0; } char sInfo[16]; menu.GetItem(param2, sInfo, sizeof(sInfo)); if (StrEqual(sInfo, "spawn")) DoSpawn(client); else if (StrEqual(sInfo, "bring")) DoBring(client); else if (StrEqual(sInfo, "goto")) DoGoto(client); else if (StrEqual(sInfo, "remove")) DoRemove(client); else if (StrEqual(sInfo, "pistol")) DoSetPistolOnly(client, !Couch_IsPistolOnly()); else if (StrEqual(sInfo, "use")) DoToggleUse(client); else if (StrEqual(sInfo, "hp")) DoSetHpEnabled(client, !Couch_IsHpEnabled()); else if (StrEqual(sInfo, "log")) DoSetLogging(client, !g_bLogging[client]); else if (StrEqual(sInfo, "effects")) { ShowEffectsMenu(client); return 0; } else if (StrEqual(sInfo, "tiers")) { ShowTierMenu(client); return 0; } else if (StrEqual(sInfo, "pushscale")) { ShowPushScaleMenu(client); return 0; } ShowCouchMenu(client); } case MenuAction_End: { delete menu; } } return 0; } // Rebuilt on every display so the labels always reflect the current state. void ShowEffectsMenu(int client) { Menu menu = new Menu(MenuHandler_Effects); menu.SetTitle("Couch - Effects"); char sItem[64]; FormatEx(sItem, sizeof(sItem), "Effect: %s", Couch_IsEffectEnabled() ? "ON" : "OFF"); menu.AddItem("effect", sItem); FormatEx(sItem, sizeof(sItem), "Beacon: %s", Couch_IsBeaconEnabled() ? "ON" : "OFF"); menu.AddItem("beacon", sItem); menu.ExitBackButton = true; menu.ExitButton = true; menu.Display(client, MENU_TIME_FOREVER); } public int MenuHandler_Effects(Menu menu, MenuAction action, int param1, int param2) { switch (action) { case MenuAction_Select: { int client = param1; if (!g_bCoreLoaded) { RequireCore(client); return 0; } char sInfo[16]; menu.GetItem(param2, sInfo, sizeof(sInfo)); if (StrEqual(sInfo, "effect")) DoToggleEffect(client); else if (StrEqual(sInfo, "beacon")) DoSetBeacon(client, !Couch_IsBeaconEnabled()); ShowEffectsMenu(client); } case MenuAction_Cancel: { if (param2 == MenuCancel_ExitBack) { if (g_bCoreLoaded) ShowCouchMenu(param1); else RequireCore(param1); } } case MenuAction_End: { delete menu; } } return 0; } void ShowTierMenu(int client) { Menu menu = new Menu(MenuHandler_Tier); menu.SetTitle("Couch - Tier requirements"); AddTierItems(menu, "move", "Move"); AddTierItems(menu, "button", "Button"); menu.ExitBackButton = true; menu.ExitButton = true; menu.Display(client, MENU_TIME_FOREVER); } // Adds "