/*  Oryx AC: collects and analyzes statistics to find some cheaters in CS:S, CS:GO, and TF2 bunnyhop.
 *  Copyright (C) 2018  Nolan O.
 *  Copyright (C) 2018  shavit.
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  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 <https://www.gnu.org/licenses/>.
*/

#include <sourcemod>
#include <sdktools>
#include <oryx>

#undef REQUIRE_PLUGIN
#include <shavit>
#include <bTimes-tas>
#include <bTimes-timer_hack>

#pragma newdecls required
#pragma semicolon 1

enum
{
	Timer_None,
	Timer_Shavit,
	Timer_Blacky2,
	Timer_Blacky183
}

int gI_Timer = Timer_None;
char gS_SpecialString[128];

ConVar gCV_AllowBypass = null;

EngineVersion gEV_Type = Engine_Unknown;

Handle gH_Forwards_OnTrigger = null;

char gS_LogPath[PLATFORM_MAX_PATH];
char gS_BeepSound[PLATFORM_MAX_PATH];
bool gB_NoSound = false;

bool gB_Testing[MAXPLAYERS+1];
bool gB_Locked[MAXPLAYERS+1];

public Plugin myinfo = 
{
	name = "ORYX bunnyhop anti-cheat",
	author = "Rusty, shavit",
	description = "Cheat detection interface.",
	version = ORYX_VERSION,
	url = "https://github.com/shavitush/Oryx-AC"
}

public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
{
	CreateNative("Oryx_CanBypass", Native_CanBypass);
	CreateNative("Oryx_Trigger", Native_OryxTrigger);
	CreateNative("Oryx_WithinThreshold", Native_WithinThreshold);
	CreateNative("Oryx_PrintToAdmins", Native_PrintToAdmins);
	CreateNative("Oryx_PrintToAdminsConsole", Native_PrintToAdminsConsole);
	CreateNative("Oryx_LogMessage", Native_LogMessage);

	// registers library, check "bool LibraryExists(const char[] name)" in order to use with other plugins
	RegPluginLibrary("oryx");

	return APLRes_Success;
}

public void OnAllPluginsLoaded()
{
	// workaround
	if(gI_Timer == Timer_None &&
		GetFeatureStatus(FeatureType_Native, "GetClientStyle") == FeatureStatus_Available &&
		GetFeatureStatus(FeatureType_Native, "Style_GetConfig") == FeatureStatus_Available)
	{
		gI_Timer = Timer_Blacky183;
	}
}

public void OnPluginStart()
{
	gH_Forwards_OnTrigger = CreateGlobalForward("Oryx_OnTrigger", ET_Event, Param_Cell, Param_CellByRef, Param_String);

	gEV_Type = GetEngineVersion();

	gCV_AllowBypass = CreateConVar("oryx_allow_bypass", "1", "Allow specific styles to bypass Oryx? Refer to README.md for information.", 0, true, 0.0, true, 1.0);

	CreateConVar("oryx_version", ORYX_VERSION, "Plugin version.", (FCVAR_NOTIFY | FCVAR_DONTRECORD));
	
	RegAdminCmd("sm_otest", Command_OryxTest, ADMFLAG_BAN, "Enables the TRIGGER_TEST detection level.");
	RegAdminCmd("sm_lock", Command_LockPlayer, ADMFLAG_BAN, "Disables movement for a player.");

	LoadTranslations("common.phrases");
	
	BuildPath(Path_SM, gS_LogPath, PLATFORM_MAX_PATH, "logs/oryx-ac.log");

	if(LibraryExists("shavit"))
	{
		gI_Timer = Timer_Shavit;
	}

	else if(LibraryExists("tas"))
	{
		gI_Timer = Timer_Blacky2;
	}
}

public void OnLibraryAdded(const char[] name)
{
	if(StrEqual(name, "shavit"))
	{
		gI_Timer = Timer_Shavit;
	}

	else if(StrEqual(name, "tas"))
	{
		gI_Timer = Timer_Blacky2;
	}
}

public void OnLibraryRemoved(const char[] name)
{
	if((StrEqual(name, "shavit") && gI_Timer == Timer_Shavit) ||
		(StrEqual(name, "tas") && gI_Timer == Timer_Blacky2))
	{
		gI_Timer = Timer_None;
	}
}

public void OnMapStart()
{
	// Beep sounds.
	Handle hConfig = LoadGameConfigFile("funcommands.games");

	if(hConfig == null)
	{
		SetFailState("Unable to load game config funcommands.games");

		return;
	}
	
	if(GameConfGetKeyValue(hConfig, "SoundBeep", gS_BeepSound, PLATFORM_MAX_PATH))
	{
		PrecacheSound(gS_BeepSound, true);
	}

	delete hConfig;
}

public void OnClientPutInServer(int client)
{
	gB_Locked[client] = false;
	gB_Testing[client] = false;
}

public Action Command_OryxTest(int client, int args)
{
	gB_Testing[client] = !gB_Testing[client];
	ReplyToCommand(client, "Testing is %s.", (gB_Testing[client])? "on":"off");

	return Plugin_Handled;
}

public Action Command_LockPlayer(int client, int args)
{
	if(args < 1)
	{
		ReplyToCommand(client, "Usage: sm_lock <target>");

		return Plugin_Handled;
	}
	
	char[] sArgs = new char[MAX_TARGET_LENGTH];
	GetCmdArgString(sArgs, MAX_TARGET_LENGTH);

	int target = FindTarget(client, sArgs);

	if(target == -1)
	{
		return Plugin_Handled;
	}
	
	gB_Locked[target] = !gB_Locked[target];
	ReplyToCommand(client, "Player has been %s.", (gB_Locked[target])? "locked":"unlocked");
	PrintToChat(target, "An admin has %s your ability to move!", (gB_Locked[target])? "locked":"unlocked");

	return Plugin_Handled;
}

public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3])
{
	// Movement is locked, don't allow anything.
	if(gB_Locked[client])
	{
		buttons = 0;
		vel[0] = 0.0;
		vel[1] = 0.0;
		impulse = 0;

		return Plugin_Changed;
	}

	return Plugin_Continue;
}

public int Native_CanBypass(Handle plugin, int numParams)
{
	if(!gCV_AllowBypass.BoolValue)
	{
		return false;
	}

	int client = GetNativeCell(1);

	switch(gI_Timer)
	{
		case Timer_Shavit:
		{
			Shavit_GetStyleStrings(Shavit_GetBhopStyle(client), sSpecialString, gS_SpecialString, 128);

			if(StrContains(gS_SpecialString, "oryx_bypass", false) != -1)
			{
				return true;
			}
		}

		case Timer_Blacky2:
		{
			return TAS_InEditMode(client);
		}

		case Timer_Blacky183:
		{
			any styleconfig[StyleConfig];
			Style_GetConfig(GetClientStyle(client), styleconfig);

			if(StrContains(styleconfig[Special_Key], "oryx_bypass", false) != -1)
			{
				return true;
			}
		}
	}

	return false;
}

public int Native_OryxTrigger(Handle plugin, int numParams)
{
	int client = GetNativeCell(1);
	int level = GetNativeCell(2);
	
	char[] sLevel = new char[16];
	char[] sCheatDescription = new char[1024];

	GetNativeString(3, sCheatDescription, 1024);

	Action result = Plugin_Continue;
	Call_StartForward(gH_Forwards_OnTrigger);
	Call_PushCell(client);
	Call_PushCellRef(level);
	Call_PushStringEx(sCheatDescription, 1024, SM_PARAM_STRING_COPY, SM_PARAM_COPYBACK);
	Call_Finish(result);

	if(result == Plugin_Stop)
	{
		return view_as<int>(Plugin_Stop);
	}

	if(level == TRIGGER_LOW)
	{
		strcopy(sLevel, 16, "LOW");
		gB_NoSound = true; // Don't play the annoying beep sound for LOW detections.
	}

	else if(level == TRIGGER_MEDIUM)
	{
		strcopy(sLevel, 16, "MEDIUM");
	}

	else if(level == TRIGGER_HIGH)
	{
		strcopy(sLevel, 16, "HIGH");

		if(result != Plugin_Handled)
		{
			//KickClient(client, "[ORYX] %s", sCheatDescription);
		}
	}

	else if(level == TRIGGER_HIGH_NOKICK)
	{
		strcopy(sLevel, 16, "HIGH-NOKICK");
	}

	else if(level == TRIGGER_DEFINITIVE)
	{
		strcopy(sLevel, 16, "DEFINITIVE");

		if(result != Plugin_Handled)
		{
			//KickClient(client, "[ORYX] %s", sCheatDescription);
		}
	}

	else if(level == TRIGGER_TEST)
	{
		char[] sBuffer = new char[128];
		Format(sBuffer, 128, "(\x03%N\x01) - %s | Level: \x04TESTING", client, sCheatDescription);

		for(int i = 1; i <= MaxClients; i++)
		{
			if(gB_Testing[i] && IsClientInGame(i))
			{
				PrintToChat(i, "%s", sBuffer);
			}
		}

		return view_as<int>(result);
	}

	char[] sAuth = new char[32];
	GetClientAuthId(client, AuthId_Steam3, sAuth, 32);

	char[] sBuffer = new char[128];
	Format(sBuffer, 128, "\x03%N\x01 - \x05%s\x01 Cheat: %s | Level: %s", client, sAuth, sCheatDescription, sLevel);
	Oryx_PrintToAdmins("%s", sBuffer);
	
	LogToFileEx(gS_LogPath, "%L - Cheat: %s | Level: %s", client, sCheatDescription, sLevel);

	return view_as<int>(result);
}

public int Native_WithinThreshold(Handle plugin, int numParams)
{
	float f1 = GetNativeCell(1);
	float f2 = GetNativeCell(2);
	float threshold = GetNativeCell(3);

	return view_as<int>(FloatAbs(f1 - f2) <= threshold);
}

public int Native_PrintToAdmins(Handle plugin, int numParams)
{
	static int iWritten = 0; // Useless?

	char[] sBuffer = new char[300];
	FormatNativeString(0, 1, 2, 300, iWritten, sBuffer);

	for(int i = 1; i <= MaxClients; i++)
	{
		if(CheckCommandAccess(i, "oryx_admin", ADMFLAG_GENERIC))
		{
			PrintToChat(i, "%s\x04[ORYX]\x01 %s", (gEV_Type == Engine_CSGO)? " ":"", sBuffer);

			if(!gB_NoSound)
			{
				if(gEV_Type == Engine_CSS || gEV_Type == Engine_TF2)
				{
					EmitSoundToClient(i, gS_BeepSound);
				}

				else
				{
					ClientCommand(i, "play */%s", gS_BeepSound);
				}
			}
		}
	}

	gB_NoSound = false;
}

public int Native_PrintToAdminsConsole(Handle plugin, int numParams)
{
	static int iWritten = 0; // Useless?

	char[] sBuffer = new char[300];
	FormatNativeString(0, 1, 2, 300, iWritten, sBuffer);

	for(int i = 1; i <= MaxClients; i++)
	{
		if(CheckCommandAccess(i, "oryx_admin", ADMFLAG_GENERIC))
		{
			PrintToConsole(i, "[ORYX] %s", sBuffer);
		}
	}
}

public int Native_LogMessage(Handle plugin, int numParams)
{
	char[] sPlugin = new char[32];

	if(!GetPluginInfo(plugin, PlInfo_Name, sPlugin, 32))
	{
		GetPluginFilename(plugin, sPlugin, 32);
	}

	static int iWritten = 0; // Useless?

	char[] sBuffer = new char[300];
	FormatNativeString(0, 1, 2, 300, iWritten, sBuffer);
	
	LogToFileEx(gS_LogPath, "[%s] %s", sPlugin, sBuffer);
}