diff --git a/gamedata/sbp.games.txt b/gamedata/sbp.games.txt
new file mode 100644
index 00000000..7c2d16c4
--- /dev/null
+++ b/gamedata/sbp.games.txt
@@ -0,0 +1,107 @@
+"Games"
+{
+ "#default"
+ {
+ "Keys"
+ {
+ "EngineInterface" "VEngineServer021"
+ }
+ "Signatures"
+ {
+ "CreateInterface"
+ {
+ "library" "engine"
+ "windows" "@CreateInterface"
+ "linux" "@CreateInterface"
+ }
+ }
+ "Offsets"
+ {
+ "ClientPrintf"
+ {
+ "windows" "45"
+ "linux" "45"
+ }
+ }
+ }
+
+ "cstrike"
+ {
+ "Keys"
+ {
+ "EngineInterface" "VEngineServer023"
+ }
+ "Offsets"
+ {
+ "ClientPrintf"
+ {
+ "windows" "45"
+ "linux" "45"
+ }
+ }
+ }
+
+ "tf"
+ {
+ "Keys"
+ {
+ "EngineInterface" "VEngineServer023"
+ }
+ "Offsets"
+ {
+ "ClientPrintf"
+ {
+ "windows" "45"
+ "linux" "45"
+ }
+ }
+ }
+
+ "csgo"
+ {
+ "Keys"
+ {
+ "EngineInterface" "VEngineServer023"
+ }
+ "Offsets"
+ {
+ "ClientPrintf"
+ {
+ "windows" "47"
+ "linux" "47"
+ }
+ }
+ }
+
+ "left4dead2"
+ {
+ "Keys"
+ {
+ "EngineInterface" "VEngineServer022"
+ }
+ "Offsets"
+ {
+ "ClientPrintf"
+ {
+ "windows" "46"
+ "linux" "46"
+ }
+ }
+ }
+
+ "nmrih"
+ {
+ "Keys"
+ {
+ "EngineInterface" "VEngineServer023"
+ }
+ "Offsets"
+ {
+ "ClientPrintf"
+ {
+ "windows" "45"
+ "linux" "45"
+ }
+ }
+ }
+}
diff --git a/scripting/adminhelpstac.sp b/scripting/adminhelpstac.sp
new file mode 100644
index 00000000..6cc75b92
--- /dev/null
+++ b/scripting/adminhelpstac.sp
@@ -0,0 +1,190 @@
+/**
+ * vim: set ts=4 :
+ * =============================================================================
+ * SourceMod Admin Help Plugin
+ * Displays and searches SourceMod commands and descriptions.
+ *
+ * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved.
+ * =============================================================================
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, version 3.0, as published by the
+ * Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see .
+ *
+ * As a special exception, AlliedModders LLC gives you permission to link the
+ * code of this program (as well as its derivative works) to "Half-Life 2," the
+ * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
+ * by the Valve Corporation. You must obey the GNU General Public License in
+ * all respects for all other code used. Additionally, AlliedModders LLC grants
+ * this exception to all derivative works. AlliedModders LLC defines further
+ * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
+ * or .
+ *
+ * Version: $Id$
+ */
+
+#pragma semicolon 1
+
+#include
+
+#pragma newdecls required
+
+#define COMMANDS_PER_PAGE 10
+
+public Plugin myinfo =
+{
+ name = "Admin Help",
+ author = "AlliedModders LLC",
+ description = "Display command information",
+ version = SOURCEMOD_VERSION,
+ url = "http://www.sourcemod.net/"
+};
+
+public void OnPluginStart()
+{
+ LoadTranslations("common.phrases");
+ LoadTranslations("adminhelp.phrases");
+ RegConsoleCmd("sm_help", HelpCmd, "Displays SourceMod commands and descriptions");
+ RegConsoleCmd("sm_searchcmd", HelpCmd, "Searches SourceMod commands");
+}
+
+public Action HelpCmd(int client, int args)
+{
+ if (client && !IsClientInGame(client))
+ {
+ return Plugin_Handled;
+ }
+
+ char arg[64], cmdName[20];
+ int pageNum = 1;
+ bool doSearch;
+
+ GetCmdArg(0, cmdName, sizeof(cmdName));
+
+ if (args >= 1)
+ {
+ GetCmdArg(1, arg, sizeof(arg));
+ StringToIntEx(arg, pageNum);
+ pageNum = (pageNum <= 0) ? 1 : pageNum;
+ }
+
+ doSearch = (strcmp("sm_help", cmdName) == 0) ? false : true;
+
+ if (GetCmdReplySource() == SM_REPLY_TO_CHAT)
+ {
+ ReplyToCommand(client, "[SM] %t", "See console for output");
+ }
+
+ char name[64];
+ char desc[255];
+ char noDesc[128];
+ CommandIterator cmdIter = new CommandIterator();
+
+ FormatEx(noDesc, sizeof(noDesc), "%T", "No description available", client);
+
+ if (doSearch)
+ {
+ int i = 1;
+ while (cmdIter.Next())
+ {
+ cmdIter.GetName(name, sizeof(name));
+ cmdIter.GetDescription(desc, sizeof(desc));
+
+ if (CheckCommandAccess(client, "sm_admin", ADMFLAG_ROOT, true))
+ {
+ if ((StrContains(name, arg, false) != -1) && CheckCommandAccess(client, name, cmdIter.Flags))
+ {
+ PrintToConsole(client, "[%03d] %s - %s", i++, name, (desc[0] == '\0') ? noDesc : desc);
+ }
+ }
+ else
+ {
+ if ((StrContains(name, arg, false) != -1) && (StrContains(name, "stac", false) == -1) && CheckCommandAccess(client, name, cmdIter.Flags))
+ {
+ PrintToConsole(client, "[%03d] %s - %s", i++, name, (desc[0] == '\0') ? noDesc : desc);
+ }
+ }
+ }
+
+ if (i == 1)
+ {
+ PrintToConsole(client, "%t", "No matching results found");
+ }
+ } else {
+ PrintToConsole(client, "%t", "SM help commands");
+
+ /* Skip the first N commands if we need to */
+ if (pageNum > 1)
+ {
+ int i;
+ int endCmd = (pageNum-1) * COMMANDS_PER_PAGE - 1;
+ for (i=0; cmdIter.Next() && i
+#include
+#include
+#include
+#include
+#undef REQUIRE_PLUGIN
+#tryinclude
+
+#pragma newdecls required
+
+public Plugin myinfo =
+{
+ name = "[DHooks] Block SM Plugins (Ricochet's Fork)",
+ description = "",
+ author = "Bara, Ricochet",
+ version = "shfjgsh625063",
+ url = "https://github.com/Bara"
+};
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ RegPluginLibrary("sbpstac");
+
+ return APLRes_Success;
+}
+
+Handle g_hClientPrintf = null;
+
+char g_sLogs[PLATFORM_MAX_PATH + 1];
+char stacVersion[32]; // The size of this may be able to be reduced.
+bool DISCORD;
+bool adminsNotified[TFMAXPLAYERS+1] = {false, ...};
+char hostname[64];
+char realSMVer[32];
+Regex smVerRegex;
+
+public void OnPluginStart()
+{
+ // Fake the admin commands for the plugins we lie about...
+ RegAdminCmd("sm_sql_addadmin", Command_DoNothing, ADMFLAG_ROOT, "Adds an admin to the SQL database");
+ RegAdminCmd("sm_sql_deladmin", Command_DoNothing, ADMFLAG_ROOT, "Removes an admin from the SQL database");
+ RegAdminCmd("sm_sql_addgroup", Command_DoNothing, ADMFLAG_ROOT, "Adds a group to the SQL database");
+ RegAdminCmd("sm_sql_delgroup", Command_DoNothing, ADMFLAG_ROOT, "Removes a group from the SQL database");
+ RegAdminCmd("sm_sql_setadmingroups", Command_DoNothing, ADMFLAG_ROOT, "Sets an admin's groups in the SQL database");
+
+ Handle gameconf = LoadGameConfigFile("sbp.games");
+ if (gameconf == null)
+ {
+ SetFailState("Failed to find sbp.games.txt gamedata");
+ delete gameconf;
+ }
+
+ int offset = GameConfGetOffset(gameconf, "ClientPrintf");
+ if (offset == -1)
+ {
+ SetFailState("Failed to find offset for ClientPrintf");
+ delete gameconf;
+ }
+
+ StartPrepSDKCall(SDKCall_Static);
+
+ if (!PrepSDKCall_SetFromConf(gameconf, SDKConf_Signature, "CreateInterface"))
+ {
+ SetFailState("Failed to get CreateInterface");
+ delete gameconf;
+ }
+
+ PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Pointer, VDECODE_FLAG_ALLOWNULL);
+ PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
+
+ char identifier[64];
+ if (!GameConfGetKeyValue(gameconf, "EngineInterface", identifier, sizeof(identifier)))
+ {
+ SetFailState("Failed to get engine identifier name");
+ delete gameconf;
+ }
+
+ Handle temp = EndPrepSDKCall();
+ Address addr = SDKCall(temp, identifier, 0);
+
+ delete gameconf;
+ delete temp;
+
+ if (!addr)
+ {
+ SetFailState("Failed to get engine ptr");
+ }
+
+ g_hClientPrintf = DHookCreate(offset, HookType_Raw, ReturnType_Void, ThisPointer_Ignore, Hook_ClientPrintf);
+ DHookAddParam(g_hClientPrintf, HookParamType_Edict);
+ DHookAddParam(g_hClientPrintf, HookParamType_CharPtr);
+ DHookRaw(g_hClientPrintf, false, addr);
+
+ char sDate[18];
+ FormatTime(sDate, sizeof(sDate), "%y-%m-%d");
+ BuildPath(Path_SM, g_sLogs, sizeof(g_sLogs), "logs/sbp-%s.log", sDate);
+}
+
+public void OnMapStart()
+{
+ smVerRegex = CompileRegex("([1-9]\\d*|0)(\\.(([1-9]\\d*)|0)){0,3}"); // Might break someday lol
+ char smVerOut[2048];
+ ServerCommandEx(smVerOut, sizeof(smVerOut), "sm version");
+ GetConVarString(FindConVar("hostname"), hostname, sizeof(hostname));
+
+ if (MatchRegex(smVerRegex, smVerOut) > 0)
+ {
+ GetRegexSubString(smVerRegex, 0, realSMVer, sizeof(realSMVer));
+ }
+ else
+ {
+ realSMVer = SOURCEMOD_VERSION;
+ }
+ CreateTimer(0.1, checkDiscord);
+}
+
+public void OnClientDisconnect(int client)
+{
+ adminsNotified[client] = false;
+}
+
+public MRESReturn Hook_ClientPrintf(Handle hParams)
+{
+ char sBuffer[1024];
+ int client = DHookGetParam(hParams, 1);
+
+ if (client == 0)
+ {
+ return MRES_Ignored;
+ }
+
+ if (IsValidAdmin(client))
+ {
+ return MRES_Ignored;
+ }
+
+ DHookGetParamString(hParams, 2, sBuffer, sizeof(sBuffer));
+ // Ideally, I wouldn't need to fake what plugin is loaded and would just remove ours from the list, but that seemingly isn't possible with this approach.
+ // DiscordAPI
+ char fakePreSQL[64] = " \"SQL Admins (Prefetch)\" (";
+ StrCat(fakePreSQL, sizeof(fakePreSQL), realSMVer);
+ StrCat(fakePreSQL, sizeof(fakePreSQL), ") by AlliedModders LLC\n");
+ //char fakePreSQL[64];
+ //Format(fakePreSQL, sizeof(fakePreSQL), " \"SQL Admins (Prefetch)\" (" ... realSMVer ... ") by AlliedModders LLC\n");
+ char discordName[] = " \"Discord API\" (1.0) by .#Zipcore, Credits: Shavit, bara, ImACow and Phire\n";
+ if (StrEqual(sBuffer, discordName))
+ {
+ notifyAdmins(client);
+ DHookSetParamString(hParams, 2, fakePreSQL);
+ return MRES_ChangedHandled;
+ }
+
+ // StAC
+ // Create fake Admin Manager string
+ char fakeAdmMan[64] = " \"SQL Admin Manager\" (";
+ StrCat(fakeAdmMan, sizeof(fakeAdmMan), realSMVer);
+ StrCat(fakeAdmMan, sizeof(fakeAdmMan), ") by AlliedModders LLC\n");
+ // Create real StAC string for strcmp
+ char stacName[128] = " \"Steph's AntiCheat [StAC]\" (";
+ StrCat(stacName, sizeof(stacName), stacVersion);
+ StrCat(stacName, sizeof(stacName), ") by https://sappho.io\n"); // May not be escaped properly. I haven't tried compiling StAC's version yet.
+ if (StrEqual(sBuffer, stacName))
+ {
+ notifyAdmins(client);
+ DHookSetParamString(hParams, 2, fakeAdmMan);
+ return MRES_ChangedHandled;
+ }
+ // SBP itself
+ // Create fake SQL Admins string
+ char fakeThrSQL[64] = " \"SQL Admins (Threaded)\" (";
+ StrCat(fakeThrSQL, sizeof(fakeThrSQL), realSMVer);
+ StrCat(fakeThrSQL, sizeof(fakeThrSQL), ") by AlliedModders LLC\n");
+ // Create SBP string for strcmp
+ char sbpName[] = " \"[DHooks] Block SM Plugins (Ricochet's Fork)\" (shfjgsh625063) by Bara, Ricochet\n";
+ if (StrEqual(sBuffer, sbpName))
+ {
+ notifyAdmins(client);
+ DHookSetParamString(hParams, 2, fakeThrSQL);
+ return MRES_ChangedHandled;
+ }
+ // Make sure Admin Help looks as if it's bundled (version number)
+ if (!StrEqual(SOURCEMOD_VERSION, realSMVer)) // No need to do any of this if the compiler version number and real version number are the same
+ {
+ // Create fake Admin Help string
+ char fakeAdmHlp[64] = " \"Admin Help\" (";
+ StrCat(fakeAdmHlp, sizeof(fakeAdmHlp), realSMVer);
+ StrCat(fakeAdmHlp, sizeof(fakeAdmHlp), ") by AlliedModders LLC\n");
+ // Create real Admin Help string for strcmp
+ char admHlpName[64] = " \"Admin Help\" (";
+ StrCat(admHlpName, sizeof(admHlpName), SOURCEMOD_VERSION);
+ StrCat(admHlpName, sizeof(admHlpName), ") by AlliedModders LLC\n");
+ if (StrEqual(sBuffer, admHlpName))
+ {
+ notifyAdmins(client);
+ DHookSetParamString(hParams, 2, fakeAdmHlp);
+ return MRES_ChangedHandled;
+ }
+ }
+ return MRES_Ignored;
+}
+
+public Action Command_DoNothing(int client, int args)
+{
+ return Plugin_Handled;
+}
+
+public void OnAllPluginsLoaded()
+{
+ GetConVarString(FindConVar("stac_version"), stacVersion, sizeof(stacVersion));
+ HookConVarChange(FindConVar("stac_version"), getStACVersion);
+}
+
+public void getStACVersion(ConVar convar, const char[] oldValue, const char[] newValue)
+{
+ if (StrEqual(stacVersion, newValue))
+ {
+ return;
+ }
+ GetConVarString(FindConVar("stac_version"), stacVersion, sizeof(stacVersion));
+}
+
+// print colored chat to all server/sourcemod admins
+void PrintToImportant(const char[] format, any ...)
+{
+ char buffer[254];
+
+ // print translations in the servers lang first
+ SetGlobalTransTarget(LANG_SERVER);
+ // format it properly
+ VFormat(buffer, sizeof(buffer), format, 2);
+ buffer[0] = '\0';
+
+ for (int i = 1; i <= MaxClients; i++)
+ {
+ if (IsValidAdmin(i))
+ {
+ SetGlobalTransTarget(i);
+ VFormat(buffer, sizeof(buffer), format, 2);
+ MC_PrintToChat(i, "%s", buffer);
+ }
+ }
+}
+
+public Action checkDiscord(Handle timer)
+{
+ // discord functionality
+ if (GetFeatureStatus(FeatureType_Native, "Discord_SendMessage") == FeatureStatus_Available)
+ {
+ DISCORD = true;
+ }
+ return Plugin_Handled;
+}
+
+void notifyAdmins(int client)
+{
+ if (adminsNotified[client])
+ {
+ return;
+ }
+ adminsNotified[client] = true;
+ PrintToImportant("{hotpink}[StAC]{white} %N accessed sm plugins", client);
+ SendMessageToDiscord(client, "Client accessed sm plugins");
+}
+
+void SendMessageToDiscord(int client, const char[] format, any ...)
+{
+
+ if (!DISCORD)
+ {
+ return;
+ }
+
+ static char generalTemplate[2048] = \
+ "{ \"embeds\": \
+ [{ \"title\": \"StAC Notification!\", \"color\": 14177041, \"fields\":\
+ [\
+ { \"name\": \"Player\", \"value\": \"%N\" } ,\
+ { \"name\": \"Message\", \"value\": \"%s\" } ,\
+ { \"name\": \"Hostname\", \"value\": \"%s\" } ,\
+ { \"name\": \"Unix timestamp\", \"value\": \"%i\" } \
+ ]\
+ }],\
+ \"avatar_url\": \"https://i.imgur.com/RKRaLPl.png\"\
+ }";
+
+ char msg[1024];
+
+ char message[256];
+ VFormat(message, sizeof(message), format, 3);
+
+ char ClName[64];
+ GetClientName(client, ClName, sizeof(ClName));
+ Discord_EscapeString(ClName, sizeof(ClName));
+
+ Format
+ (
+ msg,
+ sizeof(msg),
+ generalTemplate,
+ client,
+ message,
+ hostname,
+ GetTime()
+ );
+
+ char webhook[8] = "stac";
+ Discord_SendMessage(webhook, msg);
+}
+
+bool IsValidClient(int client)
+{
+ if
+ (
+ (0 < client <= MaxClients)
+ && IsClientInGame(client)
+ && !IsClientInKickQueue(client)
+ && !IsFakeClient(client)
+ )
+ {
+ return true;
+ }
+ return false;
+}
+
+bool IsValidAdmin(int Cl)
+{
+ if (IsValidClient(Cl))
+ {
+ if
+ (
+ CheckCommandAccess(Cl, "sm_ban", ADMFLAG_GENERIC)
+ )
+ {
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/scripting/stac.sp b/scripting/stac.sp
index 0806b96c..f96ec259 100755
--- a/scripting/stac.sp
+++ b/scripting/stac.sp
@@ -15,6 +15,7 @@
#include
#include
#include
+#include
#undef REQUIRE_PLUGIN
#tryinclude
#tryinclude
@@ -27,7 +28,7 @@
#pragma semicolon 1
#pragma newdecls required
-#define PLUGIN_VERSION "5.4.3"
+#define PLUGIN_VERSION "5.4.4"
#define UPDATE_URL "https://raw.githubusercontent.com/sapphonie/StAC-tf2/master/updatefile.txt"
@@ -70,6 +71,7 @@ public Plugin myinfo =
public void OnPluginStart()
{
StacLog("\n\n----> StAC version [%s] loaded\n", PLUGIN_VERSION);
+ CreateConVar("stac_version", PLUGIN_VERSION, "StAC version", (FCVAR_DONTRECORD));
// check if tf2, unload if not
if (GetEngineVersion() != Engine_TF2)
{
@@ -93,12 +95,12 @@ public void OnPluginStart()
}
// reg admin commands
- // TODO: make these invisible for non admins
RegConsoleCmd("sm_stac_checkall", checkAdmin, "Force check all client convars (ALL CLIENTS) for anticheat stuff");
RegConsoleCmd("sm_stac_detections", checkAdmin, "Show all current detections on all connected clients");
+ RegConsoleCmd("sm_stac_version", checkAdmin, "Prints the current active version of StAC.");
RegConsoleCmd("sm_stac_getauth", checkAdmin, "Print StAC's cached auth for a client");
RegConsoleCmd("sm_stac_livefeed", checkAdmin, "Show live feed (debug info etc) for a client. This gets printed to SourceTV if available.");
-
+ RegConsoleCmd("sm_stac_printos", checkAdmin, "Shows operating system of each connected client");
// setup regex - "Recording to ".*""
demonameRegex = CompileRegex("Recording to \".*\"");
@@ -161,6 +163,8 @@ public void OnPluginStart()
// jaypatch
OnPluginStart_jaypatch();
+ AddCommandListener(joinTeam, "jointeam");
+ AddCommandListener(joinClass, "joinclass");
}
public void OnPluginEnd()
diff --git a/scripting/stac/stac_client.sp b/scripting/stac/stac_client.sp
index f1c3520b..6e900c66 100644
--- a/scripting/stac/stac_client.sp
+++ b/scripting/stac/stac_client.sp
@@ -6,6 +6,8 @@
public void OnClientPutInServer(int Cl)
{
int userid = GetClientUserId(Cl);
+
+ timeSinceJoined[Cl] = GetEngineTime(); // Store time since joined for auto-class/team join checks
if (IsValidClientOrBot(Cl))
{
@@ -289,6 +291,8 @@ void ClearClBasedVars(int userid)
fakeAngDetects [Cl] = 0;
aimsnapDetects [Cl] = -1; // ignore first detect, it's prolly bunk
pSilentDetects [Cl] = -1; // ignore first detect, it's prolly bunk
+ invalidWishVelDetects [Cl] = -1; // ignore first detect, it's prolly bunk
+ unsyncMoveDetects [Cl] = 0;
bhopDetects [Cl] = -1; // set to -1 to ignore single jumps
cmdnumSpikeDetects [Cl] = 0;
tbotDetects [Cl] = -1; // ignore first detect, it's prolly bunk
@@ -309,8 +313,16 @@ void ClearClBasedVars(int userid)
playerTaunting [Cl] = false;
playerInBadCond [Cl] = 0;
userBanQueued [Cl] = false;
+ clientOS [Cl] = 2;
+ teamChecked [Cl] = false;
+ classChecked [Cl] = false;
+ printedOnce [Cl] = false;
// STORED SENS PER CLIENT
sensFor [Cl] = 0.0;
+ // STORED JOYSTICK SHIT
+ joystick [Cl] = false;
+ joy_xcon [Cl] = false;
+ waitTillNextQuery [Cl] = true;
// don't bother clearing arrays
LiveFeedOn [Cl] = false;
@@ -329,6 +341,8 @@ void ClearClBasedVars(int userid)
// has client has waited 60 seconds for their first cvar check
hasWaitedForCvarCheck [Cl] = false;
+ joystickQueried [Cl] = false;
+ joy_xconQueried [Cl] = false;
}
/********** TIMER FOR NETINFO **********/
diff --git a/scripting/stac/stac_commands.sp b/scripting/stac/stac_commands.sp
index 49beb819..18e16382 100755
--- a/scripting/stac/stac_commands.sp
+++ b/scripting/stac/stac_commands.sp
@@ -11,20 +11,21 @@ Action checkAdmin(int callingCl, int args)
if (callingCl != 0)
{
- bool isAdmin;
- AdminId clAdmin = GetUserAdmin(callingCl);
- if (GetAdminFlag(clAdmin, Admin_Ban))
+ if (IsValidAdmin(callingCl))
{
- isAdmin = true;
+ if (GetClientCount(true) >= 1 && !DEBUG)
+ {
+ ReplyToCommand(callingCl, "[StAC] Only one player is on. Most checks are logging only and cvar checking doesn't occur.");
+ }
}
- if (!isAdmin)
+ else
{
PrintToImportant("{hotpink}[StAC]{white} Client %N attempted to use %s, blocked access." , callingCl, arg0);
StacLogSteam(GetClientUserId(callingCl));
StacGeneralPlayerNotify(GetClientUserId(callingCl), "Client %N attempted to use %s, blocked access!", callingCl, arg0);
- return Plugin_Handled;
+ return Plugin_Continue; // Return this instead. This causes non-admins to get an "Unknown Command" message, further disguising the anticheat.
}
- StacGeneralPlayerNotify(GetClientUserId(callingCl), "Admin %N used %s", callingCl, arg0);
+ //OracxGeneralPlayerNotify(GetClientUserId(callingCl), "Admin %N used %s", callingCl, arg0); // Why should we notify for this?
}
if (StrEqual(arg0, "sm_stac_checkall"))
@@ -38,6 +39,12 @@ Action checkAdmin(int callingCl, int args)
ShowAllDetections(callingCl);
return Plugin_Handled;
}
+
+ if (StrEqual(arg0, "sm_stac_version"))
+ {
+ ShowVersion(callingCl);
+ return Plugin_Handled;
+ }
if (StrEqual(arg0, "sm_stac_getauth"))
{
@@ -59,6 +66,11 @@ Action checkAdmin(int callingCl, int args)
StacTargetCommand(callingCl, arg0, arg1);
return Plugin_Handled;
}
+ if (StrEqual(arg0, "sm_stac_printos"))
+ {
+ ShowAllOS(callingCl);
+ return Plugin_Handled;
+ }
return Plugin_Handled;
}
@@ -92,6 +104,8 @@ void ShowAllDetections(int callingCl)
|| cmdnumSpikeDetects [Cl] > 0
|| tbotDetects [Cl] > 0
|| userinfoSpamDetects [Cl] > 0
+ || invalidWishVelDetects [Cl] > 0
+ || unsyncMoveDetects [Cl] > 0
)
{
PrintToConsole
@@ -105,6 +119,8 @@ void ShowAllDetections(int callingCl)
\n pSilent %i\
\n Cmdnum spikes %i\
\n Triggerbots %i\
+ \n Invalid wish velocity %i\
+ \n Unsynchronized movement %i\
\n",
Cl,
turnTimes [Cl],
@@ -112,7 +128,9 @@ void ShowAllDetections(int callingCl)
aimsnapDetects [Cl],
pSilentDetects [Cl],
cmdnumSpikeDetects [Cl],
- tbotDetects [Cl]
+ tbotDetects [Cl],
+ invalidWishVelDetects [Cl],
+ unsyncMoveDetects [Cl]
);
}
}
@@ -122,6 +140,47 @@ void ShowAllDetections(int callingCl)
return;
}
+// sm_stac_version
+void ShowVersion(int callingCl)
+{
+ ReplyToCommand(callingCl, "StAC version [%s]", PLUGIN_VERSION);
+ return;
+}
+
+// sm_stac_printos
+void ShowAllOS(int callingCl)
+{
+ if (callingCl != 0)
+ {
+ ReplyToCommand(callingCl, "[StAC] Check your console!");
+ }
+ PrintToConsole(callingCl, "[StAC] === Printing client operating systems ===");
+ for (int Cl = 1; Cl <= MaxClients; Cl++)
+ {
+ if (IsValidClient(Cl))
+ {
+ switch(clientOS[Cl])
+ {
+ case 0:
+ {
+ PrintToConsole(callingCl, "\n%L: Windows/Wine", Cl);
+ }
+ case 1:
+ {
+ PrintToConsole(callingCl, "\n%L: Linux/MacOS", Cl);
+ }
+ default:
+ {
+ PrintToConsole(callingCl, "\n%L: Unknown, probably hasn't been queried yet.", Cl);
+ }
+ }
+ }
+ }
+ PrintToConsole(callingCl, "[StAC] === Done ===");
+
+ return;
+}
+
// sm_stac_getauth
// sm_stac_livefeed
void StacTargetCommand(int callingCl, const char[] arg0, const char[] arg1)
diff --git a/scripting/stac/stac_cvar_checks.sp b/scripting/stac/stac_cvar_checks.sp
index 705c3a93..3c594d19 100644
--- a/scripting/stac/stac_cvar_checks.sp
+++ b/scripting/stac/stac_cvar_checks.sp
@@ -30,6 +30,13 @@ char miscVars[][] =
"r_portalsopenall",
// must be == 1.0
"host_timescale",
+ // Exists on Linux only
+ "dxa_nullrefresh_capslock",
+ // Refresh this every once in a while
+ // (Supposedly) using the controller
+ "joystick",
+ // Controller plugged in
+ "joy_xcontroller_found"
// sv_force_transmit_ents ?
// sv_suppress_viewpunch ?
// tf_showspeed ?
@@ -37,8 +44,9 @@ char miscVars[][] =
};
// DEFINITE cheat vars get appended to this array.
-// Every cheat except cathook is smart enough to not have queryable cvars.
+// Every cheat except fedoraware (dead) is smart enough to not have queryable cvars.
// For now.
+// Cathook fixed their cvars being queryable but I'll keep the query in for now.
char cheatVars[][] =
{
// lith
@@ -51,6 +59,7 @@ char cheatVars[][] =
// ncc doesn't have any that i can find lol
// cathook
"cat_load",
+ "crash"
// ...melancholy? maybe? lol
// "caramelldansen",
// "SetCursor",
@@ -267,6 +276,35 @@ public void ConVarCheck(QueryCookie cookie, int Cl, ConVarQueryResult result, co
}
}
+ // dxa_nullrefresh_capslock
+ else if (StrEqual(cvarName, "dxa_nullrefresh_capslock"))
+ {
+ if (result == ConVarQuery_NotFound)
+ {
+ clientOS[Cl] = 0;
+ }
+ else
+ {
+ clientOS[Cl] = 1;
+ }
+ }
+
+ // joystick
+ else if (StrEqual(cvarName, "joystick"))
+ {
+ bool joy = (0.0 <= StringToFloat(cvarValue) < 1.0)?false:true;
+ joystick[Cl] = joy;
+ joystickQueried[Cl] = true;
+ }
+ // joy_xcontroller_found
+ else if (StrEqual(cvarName, "joy_xcontroller_found"))
+ {
+ bool joy = (0.0 <= StringToFloat(cvarValue) < 1.0)?false:true;
+ joy_xcon[Cl] = joy;
+ joy_xconQueried[Cl] = true;
+ return;
+ }
+
/*
cheat program only cvars
*/
@@ -279,7 +317,7 @@ public void ConVarCheck(QueryCookie cookie, int Cl, ConVarQueryResult result, co
}
}
// log something about cvar errors
- else if (result != ConVarQuery_Okay && !IsCheatOnlyVar(cvarName))
+ else if (result != ConVarQuery_Okay && !IsCheatOnlyVar(cvarName) && !StrEqual(cvarName, "dxa_nullrefresh_capslock"))
{
PrintToImportant("{hotpink}[StAC]{white} Could not query cvar %s on Player %N", cvarName, Cl);
StacLog("Could not query cvar %s on player %L", cvarName, Cl);
@@ -441,6 +479,12 @@ Action Timer_CheckClientConVars(Handle timer, int userid)
// query all cvars and netprops for userid
void QueryCvarsEtc(int userid, int i)
{
+ // No point in running this if only one player is on.
+ if (GetClientCount(true) == 1 && !DEBUG)
+ {
+ return;
+ }
+
// get client index of userid
int Cl = GetClientOfUserId(userid);
// don't go no further if client isn't valid!
diff --git a/scripting/stac/stac_cvars.sp b/scripting/stac/stac_cvars.sp
index 9dc40a90..b22fa527 100755
--- a/scripting/stac/stac_cvars.sp
+++ b/scripting/stac/stac_cvars.sp
@@ -64,7 +64,41 @@ void initCvars()
false,
_
);
- HookConVarChange(stac_verbose_info, setStacVars);
+ HookConVarChange(stac_ban_duration, setStacVars);
+
+ // min time before ban queue fires
+ FloatToString(minTimeBeforeBan, buffer, sizeof(buffer));
+ stac_min_time_before_ban =
+ AutoExecConfig_CreateConVar
+ (
+ "stac_min_time_before_ban",
+ buffer,
+ "[StAC] delay ban AT LEAST this long in seconds after a detection\n\
+ (recommended 15)",
+ FCVAR_NONE,
+ true,
+ 1.0,
+ false,
+ _
+ );
+ HookConVarChange(stac_min_time_before_ban, setStacVars);
+
+ // max time before ban queue fires
+ FloatToString(maxTimeBeforeBan, buffer, sizeof(buffer));
+ stac_max_time_before_ban =
+ AutoExecConfig_CreateConVar
+ (
+ "stac_max_time_before_ban",
+ buffer,
+ "[StAC] delay ban AT MOST this long in seconds after a detection\n\
+ (recommended 60)",
+ FCVAR_NONE,
+ true,
+ 2.0,
+ false,
+ _
+ );
+ HookConVarChange(stac_max_time_before_ban, setStacVars);
// turn seconds
FloatToString(maxAllowedTurnSecs, buffer, sizeof(buffer));
@@ -168,6 +202,42 @@ void initCvars()
);
HookConVarChange(stac_max_psilent_detections, setStacVars);
+ // invalid wish velocity detections
+ IntToString(maxInvalidWishVelDetections, buffer, sizeof(buffer));
+ stac_max_invalid_wish_vel_detections =
+ AutoExecConfig_CreateConVar
+ (
+ "stac_max_invalid_wish_vel_detections",
+ buffer,
+ "[StAC] maximum invalid wish velocity detections on a client before they get banned.\n\
+ -1 to disable checking for invalid wish velocity (saves cpu), 0 to print to admins/stv but never ban\n\
+ (recommended 10 or higher)",
+ FCVAR_NONE,
+ true,
+ -1.0,
+ false,
+ _
+ );
+ HookConVarChange(stac_max_invalid_wish_vel_detections, setStacVars);
+
+ // unsynchronized move detections
+ IntToString(maxUnsyncMoveDetections, buffer, sizeof(buffer));
+ stac_max_unsync_move_detections =
+ AutoExecConfig_CreateConVar
+ (
+ "stac_max_unsync_move_detections",
+ buffer,
+ "[StAC] maximum unsynchronized movement detections on a client before they get banned.\n\
+ -1 to disable checking for unsynchronized movement (saves cpu), 0 to print to admins/stv but never ban\n\
+ (recommended 10)",
+ FCVAR_NONE,
+ true,
+ -1.0,
+ false,
+ _
+ );
+ HookConVarChange(stac_max_unsync_move_detections, setStacVars);
+
// bhop detections
IntToString(maxBhopDetections, buffer, sizeof(buffer));
stac_max_bhop_detections =
@@ -497,87 +567,95 @@ void setStacVars(ConVar convar, const char[] oldValue, const char[] newValue)
SetFailState("[StAC] stac_enabled is set to 0 - aborting!");
}
- // ban duration var
- banDuration = GetConVarInt(stac_ban_duration);
+ // ban vars
+ banDuration = GetConVarInt(stac_ban_duration);
+ minTimeBeforeBan = GetConVarFloat(stac_min_time_before_ban);
+ maxTimeBeforeBan = GetConVarFloat(stac_max_time_before_ban);
// verbose info var
- DEBUG = GetConVarBool(stac_verbose_info);
+ DEBUG = GetConVarBool(stac_verbose_info);
// turn seconds var
- maxAllowedTurnSecs = GetConVarFloat(stac_max_allowed_turn_secs);
+ maxAllowedTurnSecs = GetConVarFloat(stac_max_allowed_turn_secs);
if (maxAllowedTurnSecs < 0.0 && maxAllowedTurnSecs != -1.0)
{
maxAllowedTurnSecs = 0.0;
}
// misccheats
- banForMiscCheats = GetConVarBool(stac_ban_for_misccheats);
+ banForMiscCheats = GetConVarBool(stac_ban_for_misccheats);
// optimizecvars
- optimizeCvars = GetConVarBool(stac_optimize_cvars);
+ optimizeCvars = GetConVarBool(stac_optimize_cvars);
if (optimizeCvars)
{
RunOptimizeCvars();
}
// aimsnap var
- maxAimsnapDetections = GetConVarInt(stac_max_aimsnap_detections);
+ maxAimsnapDetections = GetConVarInt(stac_max_aimsnap_detections);
// psilent var
- maxPsilentDetections = GetConVarInt(stac_max_psilent_detections);
+ maxPsilentDetections = GetConVarInt(stac_max_psilent_detections);
+
+ // invalid wish vel var
+ maxInvalidWishVelDetections = GetConVarInt(stac_max_invalid_wish_vel_detections);
+
+ // unsync move var
+ maxUnsyncMoveDetections = GetConVarInt(stac_max_unsync_move_detections);
// bhop var
- maxBhopDetections = GetConVarInt(stac_max_bhop_detections);
+ maxBhopDetections = GetConVarInt(stac_max_bhop_detections);
// fakeang var
- maxFakeAngDetections = GetConVarInt(stac_max_fakeang_detections);
+ maxFakeAngDetections = GetConVarInt(stac_max_fakeang_detections);
// cmdnum spikes var
- maxCmdnumDetections = GetConVarInt(stac_max_cmdnum_detections);
+ maxCmdnumDetections = GetConVarInt(stac_max_cmdnum_detections);
// tbot var
- maxTbotDetections = GetConVarInt(stac_max_tbot_detections);
+ maxTbotDetections = GetConVarInt(stac_max_tbot_detections);
// max ping reduce detections - clamp to -1 if 0
- maxuserinfoSpamDetections = GetConVarInt(stac_max_cmdrate_spam_detections);
+ maxuserinfoSpamDetections = GetConVarInt(stac_max_cmdrate_spam_detections);
// minterp var - clamp to -1 if 0
- min_interp_ms = GetConVarInt(stac_min_interp_ms);
+ min_interp_ms = GetConVarInt(stac_min_interp_ms);
if (min_interp_ms == 0)
{
min_interp_ms = -1;
}
// maxterp var - clamp to -1 if 0
- max_interp_ms = GetConVarInt(stac_max_interp_ms);
+ max_interp_ms = GetConVarInt(stac_max_interp_ms);
if (max_interp_ms == 0)
{
max_interp_ms = -1;
}
// min check sec var
- minRandCheckVal = GetConVarFloat(stac_min_randomcheck_secs);
+ minRandCheckVal = GetConVarFloat(stac_min_randomcheck_secs);
// max check sec var
- maxRandCheckVal = GetConVarFloat(stac_max_randomcheck_secs);
+ maxRandCheckVal = GetConVarFloat(stac_max_randomcheck_secs);
// log to file
- logtofile = GetConVarBool(stac_log_to_file);
+ logtofile = GetConVarBool(stac_log_to_file);
// properly fix pingmasking
- fixpingmasking = GetConVarBool(stac_fixpingmasking_enabled);
+ fixpingmasking = GetConVarBool(stac_fixpingmasking_enabled);
// kick unauthed clients
- kickUnauth = GetConVarBool(stac_kick_unauthed_clients);
+ kickUnauth = GetConVarBool(stac_kick_unauthed_clients);
// silent mode
- silent = GetConVarInt(stac_silent);
+ silent = 0; // This should always be 0. Setting this to anything else is BAD. Comment and I can change it if you disagree, otherwise, do it properly.
// max conns from same ip
- maxip = GetConVarInt(stac_max_connections_from_ip);
+ maxip = GetConVarInt(stac_max_connections_from_ip);
// should stac work with sv_cheats or not
- ignore_sv_cheats = GetConVarBool(stac_work_with_sv_cheats);
+ ignore_sv_cheats = GetConVarBool(stac_work_with_sv_cheats);
}
public void GenericCvarChanged(ConVar convar, const char[] oldValue, const char[] newValue)
diff --git a/scripting/stac/stac_globals.sp b/scripting/stac/stac_globals.sp
index 12695700..55ed5e2f 100644
--- a/scripting/stac/stac_globals.sp
+++ b/scripting/stac/stac_globals.sp
@@ -9,12 +9,16 @@
/***** Cvar Handles *****/
ConVar stac_enabled;
ConVar stac_ban_duration;
+ConVar stac_min_time_before_ban;
+ConVar stac_max_time_before_ban;
ConVar stac_verbose_info;
ConVar stac_max_allowed_turn_secs;
ConVar stac_ban_for_misccheats;
ConVar stac_optimize_cvars;
ConVar stac_max_aimsnap_detections;
ConVar stac_max_psilent_detections;
+ConVar stac_max_invalid_wish_vel_detections;
+ConVar stac_max_unsync_move_detections;
ConVar stac_max_bhop_detections;
ConVar stac_max_fakeang_detections;
ConVar stac_max_cmdnum_detections;
@@ -33,8 +37,10 @@ ConVar stac_max_connections_from_ip;
ConVar stac_work_with_sv_cheats;
/***** Misc cheat defaults *****/
-// ban duration
+// ban duration & banqueue times
int banDuration = 0;
+float minTimeBeforeBan = 15.0;
+float maxTimeBeforeBan = 60.0;
// verbose mode
bool DEBUG = false;
// interp
@@ -61,6 +67,8 @@ bool ignore_sv_cheats = false;
int maxAimsnapDetections = 20;
int maxPsilentDetections = 10;
int maxFakeAngDetections = 5;
+int maxInvalidWishVelDetections = 10; // Not sure what these values should be by default since they have never been widely rolled out.
+int maxUnsyncMoveDetections = 10; // ^
int maxBhopDetections = 10;
int maxCmdnumDetections = 20;
int maxTbotDetections = 0;
@@ -118,6 +126,8 @@ int turnTimes [TFMAXPLAYERS+1];
int fakeAngDetects [TFMAXPLAYERS+1];
int aimsnapDetects [TFMAXPLAYERS+1] = {-1, ...}; // set to -1 to ignore first detections, as theyre most likely junk
int pSilentDetects [TFMAXPLAYERS+1] = {-1, ...}; // ^
+int invalidWishVelDetects [TFMAXPLAYERS+1] = {-1, ...}; // ^
+int unsyncMoveDetects [TFMAXPLAYERS+1];
int bhopDetects [TFMAXPLAYERS+1] = {-1, ...}; // set to -1 to ignore single jumps
int cmdnumSpikeDetects [TFMAXPLAYERS+1];
int tbotDetects [TFMAXPLAYERS+1] = {-1, ...};
@@ -147,20 +157,33 @@ int clmouse [TFMAXPLAYERS+1] [2];
float engineTime [TFMAXPLAYERS+1][3];
float fuzzyClangles [TFMAXPLAYERS+1][5][2];
float clpos [TFMAXPLAYERS+1][2][3];
+bool joystickQueried [TFMAXPLAYERS+1] = {false, ...}; // Not sure whether or not it's necessary to set any of these.
+bool joy_xconQueried [TFMAXPLAYERS+1] = {false, ...};
+bool joystick [TFMAXPLAYERS+1] = {false, ...};
+bool joy_xcon [TFMAXPLAYERS+1] = {false, ...};
+bool printedOnce [TFMAXPLAYERS+1];
+bool waitTillNextQuery [TFMAXPLAYERS+1] = {true, ...};
// Misc stuff per client [ client index ][char size]
char SteamAuthFor [TFMAXPLAYERS+1][64];
-bool highGrav [TFMAXPLAYERS+1];
-bool playerTaunting [TFMAXPLAYERS+1];
-int playerInBadCond [TFMAXPLAYERS+1];
-bool userBanQueued [TFMAXPLAYERS+1];
-float sensFor [TFMAXPLAYERS+1];
+bool highGrav [TFMAXPLAYERS+1];
+bool playerTaunting [TFMAXPLAYERS+1];
+int playerInBadCond [TFMAXPLAYERS+1];
+int clientOS [TFMAXPLAYERS+1] = {2, ...};
+bool teamChecked [TFMAXPLAYERS+1];
+bool classChecked [TFMAXPLAYERS+1];
+bool userBanQueued [TFMAXPLAYERS+1];
+float timeSinceJoined [TFMAXPLAYERS+1];
+float timeSinceJointeam [TFMAXPLAYERS+1];
+float timeSinceJoinclass [TFMAXPLAYERS+1];
+bool userBanQueued [TFMAXPLAYERS+1];
+float sensFor [TFMAXPLAYERS+1];
// weapon name, gets passed to aimsnap check
-char hurtWeapon [TFMAXPLAYERS+1][256];
-char lastCommandFor [TFMAXPLAYERS+1][256];
-bool LiveFeedOn [TFMAXPLAYERS+1];
-bool hasBadName [TFMAXPLAYERS+1];
+char hurtWeapon [TFMAXPLAYERS+1][256];
+char lastCommandFor [TFMAXPLAYERS+1][256];
+bool LiveFeedOn [TFMAXPLAYERS+1];
+bool hasBadName [TFMAXPLAYERS+1];
// network info
float lossFor [TFMAXPLAYERS+1];
@@ -189,6 +212,7 @@ Handle HudSyncNetwork;
// Timer handles
Handle QueryTimer [TFMAXPLAYERS+1];
+Handle BanTimer [TFMAXPLAYERS+1];
Handle TriggerTimedStuffTimer;
/*
diff --git a/scripting/stac/stac_mapchange.sp b/scripting/stac/stac_mapchange.sp
index 26ce5ced..bfd6339a 100644
--- a/scripting/stac/stac_mapchange.sp
+++ b/scripting/stac/stac_mapchange.sp
@@ -10,6 +10,7 @@ public void OnConfigsExecuted()
public void OnMapStart()
{
+ checkForBadPlugins(); // Not sure if this is the best place for this.
OpenStacLog();
ActuallySetRandomSeed();
DoTPSMath();
@@ -25,6 +26,11 @@ public void OnMapStart()
GetConVarString(FindConVar("hostname"), hostname, sizeof(hostname));
}
+public void OnAllPluginsLoaded()
+{
+ checkForBadPlugins(); // Also not sure on this.
+}
+
public Action eRoundStart(Handle event, char[] name, bool dontBroadcast)
{
DoTPSMath();
@@ -229,3 +235,23 @@ void DoTPSMath()
StacLog("tickinterv %f, tps %f", tickinterv, tps);
}
}
+
+void checkForBadPlugins()
+{
+ // Make sure we're not using the original adminhelp.
+ if (FindPluginByFile("adminhelp.smx") != INVALID_HANDLE)
+ {
+ char message[] = "StAC failed to load because a possibly unmodified \"adminhelp\" was found.";
+ PrintToImportant(message);
+ SendMessageToDiscord(message);
+ SetFailState("Unmodified(?) adminhelp copy is still present (Unexpected adminhelp.smx). Delete it from the folder, use \"sm plugins unload adminhelp\", and reload StAC.");
+ }
+ // Make sure we're not using the original SBP.
+ if (FindPluginByFile("sbp.smx") != INVALID_HANDLE)
+ {
+ char message[] = "StAC failed to load because a possibly unmodified \"sbp\" was found.";
+ PrintToImportant(message);
+ SendMessageToDiscord(message);
+ SetFailState("Unmodified(?) sbp copy is still present (Unexpected sbp.smx). Delete it from the folder, use \"sm plugins unload sbp\", and reload StAC.");
+ }
+}
diff --git a/scripting/stac/stac_misc_checks.sp b/scripting/stac/stac_misc_checks.sp
index 664b5633..d6eaac87 100644
--- a/scripting/stac/stac_misc_checks.sp
+++ b/scripting/stac/stac_misc_checks.sp
@@ -37,6 +37,44 @@ public Action OnClientSayCommand(int Cl, const char[] command, const char[] sArg
return Plugin_Continue;
}
+Action joinTeam(int Cl, const char[] command, int argc)
+{
+ if (!teamChecked[Cl])
+ {
+ char ClName[64];
+ int userid = GetClientUserId(Cl);
+ GetClientName(Cl, ClName, sizeof(ClName));
+ timeSinceJointeam[Cl] = GetEngineTime();
+ if (timeSinceJointeam[Cl] - timeSinceJoined[Cl] < 2.5)
+ {
+ PrintToImportant("Suspicious: %.2f seconds between %s fully joined and chose team", timeSinceJointeam[Cl] - timeSinceJoined[Cl], ClName);
+ StacGeneralPlayerNotify(userid, "Suspicious: %.2f seconds between %s fully joined and chose team", timeSinceJointeam[Cl] - timeSinceJoined[Cl], ClName);
+ }
+ teamChecked[Cl] = true;
+ return Plugin_Continue;
+ }
+ return Plugin_Continue;
+}
+
+Action joinClass(int Cl, const char[] command, int argc)
+{
+ if (!classChecked[Cl])
+ {
+ char ClName[64];
+ int userid = GetClientUserId(Cl);
+ GetClientName(Cl, ClName, sizeof(ClName));
+ timeSinceJoinclass[Cl] = GetEngineTime();
+ if (timeSinceJoinclass[Cl] - timeSinceJoined[Cl] < 2.5) // This value may need to be tweaked.
+ {
+ PrintToImportant("Suspicious: %.2f seconds between %s fully joined and chose class", timeSinceJoinclass[Cl] - timeSinceJoined[Cl], ClName);
+ StacGeneralPlayerNotify(userid, "Suspicious: %.2f seconds between %s fully joined and chose class", timeSinceJoinclass[Cl] - timeSinceJoined[Cl], ClName);
+ }
+ classChecked[Cl] = true;
+ return Plugin_Continue;
+ }
+ return Plugin_Continue;
+}
+
void NameCheck(int userid)
{
int Cl = GetClientOfUserId(userid);
diff --git a/scripting/stac/stac_onplayerruncmd.sp b/scripting/stac/stac_onplayerruncmd.sp
index 3e00058a..7b5457cb 100755
--- a/scripting/stac/stac_onplayerruncmd.sp
+++ b/scripting/stac/stac_onplayerruncmd.sp
@@ -9,6 +9,9 @@
- AIM SNAPS
- FAKE ANGLES
- TURN BINDS
+ - BHOP CHEATS
+ - INVALID WISH VELOCITY (can and will false positive on +strafe (left alt key by default))
+ - UNSYNCHRONIZED MOVEMENT (will false positive if cl_yawspeed = 0 and client uses turnbinds (+right, +left) and +strafe.)
*/
public void OnPlayerRunCmdPre
@@ -47,10 +50,13 @@ public Action OnPlayerRunCmd
OnPlayerRunCmd_jaypatch(Cl, buttons, impulse, vel, angles, weapon, subtype, cmdnum, tickcount, seed, mouse);
// sanity check, don't let banned clients do anything!
+ // This lets them know something is up immediately, removing the point of the banqueue.
+ /*
if (userBanQueued[Cl])
{
return Plugin_Handled;
}
+ */
return Plugin_Continue;
}
@@ -133,7 +139,7 @@ stock void PlayerRunCmd
}
clcmdnum[Cl][0] = cmdnum;
- // grab tickccount
+ // grab tickcount
for (int i = 5; i > 0; --i)
{
cltickcount[Cl][i] = cltickcount[Cl][i-1];
@@ -226,10 +232,10 @@ stock void PlayerRunCmd
if
(
// make sure client doesn't have invalid angles. "invalid" in this case means "any angle is 0.000000", usually caused by plugin / trigger based teleportation
- !HasValidAngles(Cl)
+ !HasValidAngles(Cl) // This is actively abused to bypass, according to untrustworthy sources! But, I do see it as very possible.
// make sure client doesn't have OUTRAGEOUS ping
// most cheater fakeping goes up to 800 so tack on 50 just in case
- || pingFor[Cl] > 850.0
+ || pingFor[Cl] > 850.0 // I really don't think this is necessary. The other lag checks should prevent any fucky stuff from happening, but what do I know.
)
{
return;
@@ -244,8 +250,8 @@ stock void PlayerRunCmd
if
(
// make sure client isnt using a spin bind
- buttons & IN_LEFT
- || buttons & IN_RIGHT
+ buttons & IN_LEFT // Almost certainly an easy bypass.
+ || buttons & IN_RIGHT // ^
// make sure we're not lagging and that cmdnum is saneish
|| IsUserLagging(userid, true, false)
)
@@ -256,12 +262,186 @@ stock void PlayerRunCmd
aimsnapCheck(userid);
triggerbotCheck(userid);
psilentCheck(userid);
+ invalidWishVelCheck(userid, vel[0], vel[1], buttons); // In theory, this doesn't need to be down here. I'm only worried about halloween conditions causing issues for this.
+ unsynchronizedMoveCheck(userid, buttons, vel[0], vel[1]);
return;
}
+/*
+ INVALID WISH VELOCITY CHECK
+ Thanks to Oryx and the devs behind it for this check.
+ Actually implemented it into the anticheat properly this time!
+ * Copyright (C) 2018 Nolan O.
+ * Copyright (C) 2018 shavit.
+*/
+void invalidWishVelCheck(int userid, float forwardmove, float sidemove, int buttons)
+{
+ int Cl = GetClientOfUserId(userid);
+
+ int attack = 0;
+ if ((clbuttons[Cl][0] & IN_ATTACK))
+ {
+ attack = 1;
+ }
+ else if ((clbuttons[Cl][0] & IN_ATTACK2))
+ {
+ attack = 2;
+ }
+
+ if
+ (
+ (
+ // The absolute value of forwardmove and sidemove should NEVER be greater than 450 (or 450.00003 for some reason...)
+ (
+ FloatAbs(forwardmove) > 450.00003
+ ||
+ FloatAbs(sidemove) > 450.00003
+ /*
+ Reasoning for the sidemove/forwardmove value of 450.00003:
+ Essentially, some legit clients report a value of 450.0000305175 (even happened to my client), which IS greater than 450.0. Why does this happen? No clue.
+ I just wanted to add a bit of leniency. Honestly, this isn't enough to cause any issues.
+ This could also be caused by +strafe, but who knows.
+ */
+ )
+ )
+ &&
+ (
+ clangles[Cl][1][1] != clangles[Cl][0][1] // (somewhat) Workaround false positives occuring while holding +strafe. Technically may open up room for a bypass, but I have found no other solution.
+ )
+ )
+ {
+ invalidWishVelDetects[Cl]++;
+ if (invalidWishVelDetects[Cl] > 0) // First detect is ignored.
+ {
+ PrintToImportant
+ (
+ "{hotpink}[StAC]{white} Player %N {mediumpurple}has invalid wish velocity{white}!\
+ \nThis player is *probably* cheating, especially if they trigger this more than once.\
+ \nDetections so far: {palegreen}%i",
+ Cl,
+ invalidWishVelDetects[Cl]
+ );
+ PrintToImportant
+ (
+ "{white}Forwardmove: {palegreen}%.10f, {white}Sidemove: {palegreen}%.10f",
+ forwardmove,
+ sidemove
+ );
+ if (attack > 0)
+ {
+ PrintToImportant
+ (
+ "{hotpink}[StAC]{red} This player was ATTACKING (ATTACK%i), \
+ you should ban them manually with the reason \"Banned from server\"",
+ attack
+ );
+ }
+ StacLogSteam(userid);
+ if (invalidWishVelDetects[Cl] == 1 || invalidWishVelDetects[Cl] % 5 == 0)
+ {
+ char invalidWishVelInfo[128];
+ if (attack > 0)
+ {
+ Format
+ (
+ invalidWishVelInfo,
+ sizeof(invalidWishVelInfo),
+ "invalid wish velocity WHILE ATTACKING (attack%i, forwardmove: %.10f, sidemove %.10f)",
+ attack,
+ forwardmove,
+ sidemove
+ );
+ }
+ else
+ {
+ Format
+ (
+ invalidWishVelInfo,
+ sizeof(invalidWishVelInfo),
+ "invalid wish velocity (forwardmove: %.10f, sidemove %.10f)",
+ forwardmove,
+ sidemove
+ );
+ }
+ StacDetectionNotify(userid, invalidWishVelInfo, invalidWishVelDetects[Cl]);
+ }
+ if (invalidWishVelDetects[Cl] >= maxInvalidWishVelDetections && maxInvalidWishVelDetections > 0)
+ {
+ char reason[128];
+ Format(reason, sizeof(reason), "%t", "invalidWishVelBanMsg", invalidWishVelDetects[Cl]);
+ char pubreason[256];
+ Format(pubreason, sizeof(pubreason), "%t", "invalidWishVelBanAllChat", Cl, invalidWishVelDetects[Cl]);
+ BanUser(userid, reason, pubreason);
+ }
+ }
+ }
+}
+
+/*
+ UNSYNCHRONIZED MOVEMENT CHECK -- Catches autostrafers
+ Thanks to Oryx and the devs behind it for this check.
+ * Copyright (C) 2018 Nolan O.
+ * Copyright (C) 2018 shavit.
+*/
+// If it was my choice, I would ban controller users and cheaters all the same since they all trigger this. Plus, controller players are annoying.
+void unsynchronizedMoveCheck(int userid, int buttons, float forwardmove, float sidemove)
+{
+ int Cl = GetClientOfUserId(userid);
+ if (playerUsingController(userid))
+ {
+ return;
+ }
+
+ if
+ (
+ // don't bother checking if unsync'd move detection is off
+ maxUnsyncMoveDetections != -1
+ &&
+ // Unsynchronized usercmd->forwardmove or usercmd->sidemove.
+ // cl_forwardspeed and cl_sidespeed are the fully-pressed move values.
+ // The game will never apply them unless the buttons are added into the usercmd too. (exceptions: controllers)
+ // https://mxr.alliedmods.net/hl2sdk-css/source/game/client/in_main.cpp#557
+ // https://mxr.alliedmods.net/hl2sdk-css/source/game/client/in_main.cpp#842
+ (
+ (forwardmove == 450.0 && (buttons & IN_FORWARD) == 0) || // Check for unsynchronized forwards movement
+ (sidemove == -450.0 && (buttons & IN_MOVELEFT) == 0) || // Check for unsynchronized sideways (left) movement
+ (forwardmove == -450.0 && (buttons & IN_BACK) == 0) || // Check for unsynchronized backwards movement
+ (sidemove == 450.0 && (buttons & IN_MOVERIGHT) == 0) // Check for unsynchronized sideways (right) movement
+ )
+ )
+ {
+ unsyncMoveDetects[Cl]++;
+ PrintToImportant
+ (
+ "{hotpink}[StAC]{white} Player %N {mediumpurple}had unsynchronized movement{white}!\
+ \nConsecutive detections so far: {palegreen}%i",
+ Cl,
+ unsyncMoveDetects[Cl]
+ );
+ PrintToImportant("{hotpink}[StAC]{red} Hey, since this doesn't ban automatically yet (testing mode), you should ban them manually with the reason \"Banned from server\". This goes for any detection, but especially this one.");
+ StacLogSteam(userid);
+
+ if (unsyncMoveDetects[Cl] == 1 || unsyncMoveDetects[Cl] % 5 == 0)
+ {
+ char unsyncMovInfo[128];
+ Format(unsyncMovInfo, sizeof(unsyncMovInfo), "unsynchronized movement");
+ StacDetectionNotify(userid, unsyncMovInfo, unsyncMoveDetects[Cl]);
+ }
+ if (unsyncMoveDetects[Cl] >= maxUnsyncMoveDetections && maxUnsyncMoveDetections > 0)
+ {
+ char reason[128];
+ Format(reason, sizeof(reason), "%t", "unsynchronizedMoveBanMsg", unsyncMoveDetects[Cl]);
+ char pubreason[256];
+ Format(pubreason, sizeof(pubreason), "%t", "unsynchronizedMoveBanAllChat", Cl, unsyncMoveDetects[Cl]);
+ BanUser(userid, reason, pubreason);
+ }
+ }
+}
+
/*
BHOP DETECTION - using lilac and ssac as reference, this one's better tho
+ There is a better way to do this without notifying the client that they have been bhop checked. (Can be abused to detect StAC running on the server) Look at Oryx/Bash2.
*/
void bhopCheck(int userid)
{
@@ -337,7 +517,7 @@ void bhopCheck(int userid)
Format(reason, sizeof(reason), "%t", "bhopBanMsg", bhopDetects[Cl]);
char pubreason[256];
Format(pubreason, sizeof(pubreason), "%t", "bhopBanAllChat", Cl, bhopDetects[Cl]);
- BanUser(userid, reason, pubreason);
+ BanUserOriginal(userid, reason, pubreason); // We use the original version of this function because you can tell when you've triggered it, therefore leading to discovery of a ban queue system.
return;
}
@@ -621,7 +801,8 @@ void psilentCheck(int userid)
// doing this might make it harder to detect legitcheaters but like. legitcheating in a 12 yr old dead game OMEGALUL who fucking cares
if
(
- aDiffReal >= 1.0 && fuzzy >= 0
+ //aDiffReal >= 0.5 && fuzzy >= 0 // I care about legitcheating in a 12 yr old "dead game". I want a fair game. If a client triggers this consistently, especially at a consistent angle value, they are almost certainly cheating. The anticheat handles false positives already.
+ fuzzy >= 0
)
{
pSilentDetects[Cl]++;
@@ -651,6 +832,8 @@ void psilentCheck(int userid)
}
if (pSilentDetects[Cl] % 5 == 0)
{
+ char pSilentInfo[128];
+ Format(pSilentInfo, sizeof(pSilentInfo), "psilent (anglediff: %.2f°, fuzzy: %s, norecoil: %s)", aDiffReal, fuzzy == 1 ? "yes" : "no", aDiffReal <= 3.0 ? "yes" : "no");
StacDetectionNotify(userid, "psilent", pSilentDetects[Cl]);
}
// BAN USER if they trigger too many detections
@@ -1049,6 +1232,37 @@ bool isTickcountRepeated(int userid)
return false;
}
+// I would like to check if the player has controller-like movements someday, because if this goes open source, people will force these cvars to avoid detection.
+bool playerUsingController(int userid)
+{
+ int Cl = GetClientOfUserId(userid);
+ // If we haven't queried these cvars from this client, we don't know if the player is using a controller or not yet.
+ if (!joystickQueried[Cl] || !joy_xconQueried[Cl])
+ {
+ return true;
+ }
+ // If the player has joystick set to 1 (enables controller) and has joy_xcon set to 1 (controller is plugged in), they are using a controller.
+ if (joystick[Cl] && joy_xcon[Cl])
+ {
+ if (!printedOnce[Cl])
+ {
+ PrintToImportant("{hotpink}[StAC]{white} Player %N {powderblue}appears to be using a controller{white}!", Cl);
+ StacGeneralPlayerNotify(userid, "Client appears to be using a controller");
+ printedOnce[Cl] = true;
+ }
+ return true;
+ }
+ // Make sure we don't detect on people who plug their controller in mid-game. This will make any checks calling this take longer to kick in, but prevent false positives.
+ if (waitTillNextQuery[Cl])
+ {
+ waitTillNextQuery[Cl] = false;
+ joystickQueried[Cl] = false;
+ joy_xconQueried[Cl] = false;
+ return true;
+ }
+ return false;
+}
+
/********** DETECTION FORGIVENESS TIMERS **********/
Action Timer_decr_aimsnaps(Handle timer, any userid)
diff --git a/scripting/stac/stac_stocks.sp b/scripting/stac/stac_stocks.sp
index 52270658..a63fd4c8 100644
--- a/scripting/stac/stac_stocks.sp
+++ b/scripting/stac/stac_stocks.sp
@@ -381,18 +381,84 @@ bool IsValidSrcTV(int client)
/********** MISC FUNCS **********/
-void BanUser(int userid, char[] reason, char[] pubreason)
+// Thanks to Mitchell on AlliedModders for this.
+stock int FindClientBySteamID(char[] SteamID)
+{
+ char steamid[64];
+ for (int Cl = 1; Cl <= MaxClients; Cl++)
+ {
+ if (IsClientInGame(Cl))
+ {
+ GetClientAuthId(Cl, AuthId_Steam2, steamid, sizeof(steamid));
+ if (StrEqual(steamid, SteamID, false))
+ {
+ return Cl;
+ }
+ }
+ }
+ return -1;
+}
+
+Action Timer_BanAfterTime(Handle timer, DataPack pack)
+{
+ int userid;
+ int Cl;
+ char reason[128];
+ char pubreason[256];
+ char bannedID[TFMAXPLAYERS+1][64];
+ char steamid[64];
+ // We probably want to set this to something so that if Cl == 0, we don't error out on the strcmp because there is nothing stored in the string.
+ steamid = "STEAM_1:1:00000000";
+
+ pack.Reset();
+ userid = pack.ReadCell();
+ Cl = GetClientOfUserId(userid);
+ pack.ReadString(reason, sizeof(reason));
+ pack.ReadString(pubreason, sizeof(pubreason));
+ pack.ReadString(bannedID[Cl], sizeof(bannedID));
+
+ if (Cl != 0)
+ {
+ GetClientAuthId(Cl, AuthId_Steam2, steamid, sizeof(steamid)); // Get the client's SteamID
+ }
+
+ if (strcmp(steamid, bannedID[Cl]) == 0) // Make sure we're not banning the wrong client.
+ {
+ BanUserOriginal(userid, reason, pubreason); // Why wasn't I doing this before? Lol.
+ }
+ else
+ {
+ int bannedClient = FindClientBySteamID(bannedID[Cl]);
+ if (bannedClient == -1)
+ {
+ BanIdentity(bannedID[Cl], 0, BANFLAG_AUTHID, "Banned from server"); // User isn't connected to the server. Add to banlist.
+ }
+ else
+ {
+ int newuserid = GetClientUserId(bannedClient);
+ BanUserOriginal(newuserid, reason, pubreason); // Client is connected to the server. Ban and kick them.
+ }
+ }
+ return Plugin_Continue;
+}
+
+/*
+ We need this still.
+*/
+void BanUserOriginal(int userid, char[] reason, char[] pubreason)
{
int Cl = GetClientOfUserId(userid);
// prevent double bans
+ /*
if (userBanQueued[Cl])
{
- KickClient(Cl, "Banned by StAC");
+ KickClient(Cl, "Banned from server");
return;
- }
+ }
+ */
- StacGeneralPlayerNotify(userid, reason);
+ // OracxGeneralPlayerNotify(userid, reason);
// make sure we dont detect on already banned players
userBanQueued[Cl] = true;
@@ -418,22 +484,22 @@ void BanUser(int userid, char[] reason, char[] pubreason)
{
if (SOURCEBANS)
{
- SBPP_BanPlayer(0, Cl, banDuration, reason);
+ SBPP_BanPlayer(0, Cl, banDuration, "Banned from server");
// there's no return value for that native, so we have to just assume it worked lol
return;
}
- if (MATERIALADMIN && MABanPlayer(0, Cl, MA_BAN_STEAM, banDuration, reason))
+ if (MATERIALADMIN && MABanPlayer(0, Cl, MA_BAN_STEAM, banDuration, "Banned from server"))
{
return;
}
if (GBANS)
{
- ServerCommand("gb_ban %i, %i, %s", userid, banDuration, reason);
+ ServerCommand("gb_ban %i, %i, %s", userid, banDuration, "Banned from server");
// there's no return value nor a native for gbans bans (YET), so we have to just assume it worked lol
return;
}
// stock tf2, no ext ban system. if we somehow fail here, keep going.
- if (BanClient(Cl, banDuration, BANFLAG_AUTO, reason, reason, _, _))
+ if (BanClient(Cl, banDuration, BANFLAG_AUTO, reason, "Banned from server", _, _))
{
return;
}
@@ -443,8 +509,8 @@ void BanUser(int userid, char[] reason, char[] pubreason)
// if this returns true, we can still ban the client with their steamid in a roundabout and annoying way.
if (!IsActuallyNullString(SteamAuthFor[Cl]))
{
- ServerCommand("sm_addban %i \"%s\" %s", banDuration, SteamAuthFor[Cl], reason);
- KickClient(Cl, "%s", reason);
+ ServerCommand("sm_addban %i \"%s\" %s", banDuration, SteamAuthFor[Cl], "Banned from server");
+ KickClient(Cl, "%s", "Banned from server");
}
// if the above returns false, we can only do ip :/
else
@@ -453,12 +519,13 @@ void BanUser(int userid, char[] reason, char[] pubreason)
GetClientIP(Cl, ip, sizeof(ip));
StacLog("No cached SteamID for %N! Banning with IP %s...", Cl, ip);
- ServerCommand("sm_banip %s %i %s", ip, banDuration, reason);
+ ServerCommand("sm_banip %s %i %s", ip, banDuration, "Banned from server");
// this kick client might not be needed - you get kicked by "being added to ban list"
// KickClient(Cl, "%s", reason);
}
- MC_PrintToChatAll("%s", pubreason);
+ // MC_PrintToChatAll("%s", pubreason); This still isn't happening. Why should we notify all clients WHY they were banned? Bad idea.
+ PrintToImportant("%s", pubreason); // This may be redundant. Testing needed.
StacLog("%s", pubreason);
}
@@ -707,16 +774,17 @@ void StacGeneralPlayerNotify(int userid, const char[] format, any ...)
static char generalTemplate[2048] = \
"{ \"embeds\": \
- [{ \"title\": \"StAC Detection!\", \"color\": 16738740, \"fields\":\
+ [{ \"title\": \"StAC Notification!\", \"color\": 16738740, \"fields\":\
[\
- { \"name\": \"Player\", \"value\": \"%N\" } ,\
- { \"name\": \"SteamID\", \"value\": \"%s\" } ,\
- { \"name\": \"Message\", \"value\": \"%s\" } ,\
- { \"name\": \"Hostname\", \"value\": \"%s\" } ,\
- { \"name\": \"Server IP\", \"value\": \"%s\" } ,\
- { \"name\": \"Current Demo\", \"value\": \"%s\" } ,\
- { \"name\": \"Demo Tick\", \"value\": \"%i\" } ,\
- { \"name\": \"Unix timestamp\", \"value\": \"%i\" } \
+ { \"name\": \"Player\", \"value\": \"%N\" } ,\
+ { \"name\": \"SteamID\", \"value\": \"%s\" } ,\
+ { \"name\": \"Operating System\", \"value\": \"%s\" } ,\
+ { \"name\": \"Message\", \"value\": \"%s\" } ,\
+ { \"name\": \"Hostname\", \"value\": \"%s\" } ,\
+ { \"name\": \"Server IP\", \"value\": \"%s\" } ,\
+ { \"name\": \"Current Demo\", \"value\": \"%s\" } ,\
+ { \"name\": \"Demo Tick\", \"value\": \"%i\" } ,\
+ { \"name\": \"Unix timestamp\", \"value\": \"%i\" } \
]\
}],\
\"avatar_url\": \"https://i.imgur.com/RKRaLPl.png\"\
@@ -745,6 +813,24 @@ void StacGeneralPlayerNotify(int userid, const char[] format, any ...)
{
steamid = "N/A";
}
+
+ char OS[36]; // 34 bytes (largest outcome) + 1 (string read end byte) + 1 (to make it an even number lol) Note: I don't know anything about how to handle arrays!
+ switch(clientOS[Cl])
+ {
+ case 0:
+ {
+ Format(OS, sizeof(OS), "Windows/Wine");
+ }
+ case 1:
+ {
+ Format(OS, sizeof(OS), "Linux/MacOS (Most likely Cathook)");
+ }
+ default:
+ {
+ Format(OS, sizeof(OS), "Not queried yet or blocked by user");
+ }
+ }
+
Format
(
msg,
@@ -752,6 +838,7 @@ void StacGeneralPlayerNotify(int userid, const char[] format, any ...)
generalTemplate,
Cl,
steamid,
+ OS,
message,
hostname,
hostipandport,
@@ -776,15 +863,16 @@ void StacDetectionNotify(int userid, char[] type, int detections)
"{ \"embeds\": \
[{ \"title\": \"StAC Detection!\", \"color\": 16738740, \"fields\":\
[\
- { \"name\": \"Player\", \"value\": \"%N\" } ,\
- { \"name\": \"SteamID\", \"value\": \"%s\" } ,\
- { \"name\": \"Detection type\", \"value\": \"%s\" } ,\
- { \"name\": \"Detection\", \"value\": \"%i\" } ,\
- { \"name\": \"Hostname\", \"value\": \"%s\" } ,\
- { \"name\": \"Server IP\", \"value\": \"%s\" } ,\
- { \"name\": \"Current Demo\", \"value\": \"%s\" } ,\
- { \"name\": \"Demo Tick\", \"value\": \"%i\" } ,\
- { \"name\": \"Unix timestamp\", \"value\": \"%i\" } \
+ { \"name\": \"Player\", \"value\": \"%N\" } ,\
+ { \"name\": \"SteamID\", \"value\": \"%s\" } ,\
+ { \"name\": \"Operating System\", \"value\": \"%s\" } ,\
+ { \"name\": \"Detection type\", \"value\": \"%s\" } ,\
+ { \"name\": \"Detection\", \"value\": \"%i\" } ,\
+ { \"name\": \"Hostname\", \"value\": \"%s\" } ,\
+ { \"name\": \"Server IP\", \"value\": \"%s\" } ,\
+ { \"name\": \"Current Demo\", \"value\": \"%s\" } ,\
+ { \"name\": \"Demo Tick\", \"value\": \"%i\" } ,\
+ { \"name\": \"Unix timestamp\", \"value\": \"%i\" } \
]\
}],\
\"avatar_url\": \"https://i.imgur.com/RKRaLPl.png\"\
@@ -810,6 +898,23 @@ void StacDetectionNotify(int userid, char[] type, int detections)
{
steamid = "N/A";
}
+
+ char OS[36]; // 34 bytes (largest outcome) + 1 (string read end byte) + 1 (to make it an even number lol) Note: I don't know anything about how to handle arrays!
+ switch(clientOS[Cl])
+ {
+ case 0:
+ {
+ Format(OS, sizeof(OS), "Windows/Wine");
+ }
+ case 1:
+ {
+ Format(OS, sizeof(OS), "Linux/MacOS (Most likely Cathook)");
+ }
+ default:
+ {
+ Format(OS, sizeof(OS), "Not queried yet or blocked by user");
+ }
+ }
Format
(
@@ -818,6 +923,7 @@ void StacDetectionNotify(int userid, char[] type, int detections)
detectionTemplate,
Cl,
steamid,
+ OS,
type,
detections,
hostname,
diff --git a/translations/stac.phrases.txt b/translations/stac.phrases.txt
index 9e8e80db..7c90c8db 100755
--- a/translations/stac.phrases.txt
+++ b/translations/stac.phrases.txt
@@ -366,6 +366,26 @@
"es" "[StAC] Baneado por valores de logros falsos"
"de" "[StAC] Gebannt für falsche Achievements"
}
+ "invalidWishVelBanAllChat"
+ {
+ "#format" "{1:N},{2:i}"
+ "en" "{hotpink}[StAC]{white} Player {1} had {mediumpurple}invalid wish velocity{white} using a {mediumpurple}cheat program{white}. Total invalid wish velocities: {mediumpurple}{2}{white}. {palegreen}Queued for ban!"
+ }
+ "invalidWishVelBanMsg"
+ {
+ "#format" "{1:i}"
+ "en" "[StAC] Banned for invalid wish velocity after {1} detections"
+ }
+ "unsynchronizedMoveBanAllChat"
+ {
+ "#format" "{1:N},{2:i}"
+ "en" "{hotpink}[StAC]{white} Player {1} had {mediumpurple}unsynchronized movement{white}. Total unsynchronized movements: {mediumpurple}{2}{white}. {palegreen}Queued for ban!"
+ }
+ "unsynchronizedMoveBanMsg"
+ {
+ "#format" "{1:i}"
+ "en" "[StAC] Banned for unsynchronized movement after {1} detections"
+ }
// newlines in chat
"newlineBanAllChat"
{